ablauf
v0.0.4
Published
Workflow engine for Nuxt
Readme
Ablauf
A Nuxt module for defining state machines with typed states, directional transitions, and pluggable handler pipelines.
Features
- Define workflows as JSON with states, transitions, and directions
- Transition handler pipelines — run validation, confirmation, or side-effects before a state change
- Generic
conditionsbag on transitions for custom domain logic (e.g.nativeOnly,billed) - Pluggable storage via
WorkflowProvider— built-in file provider (bundles JSON into the server build at compile time, works across every Nitro deploy preset), or bring your own (database, API, etc.) useWorkflowcomposable withgetNextStates,findTransition,transition, and more- Auto-imported types, composables, and server utilities
- Hot-reload for workflow definitions and transition handlers in dev
Quick Setup
Install the module:
npx nuxt module add ablaufAdd it to your nuxt.config.ts:
export default defineNuxtConfig({
modules: ['ablauf'],
workflow: {
provider: 'file', // 'file' (default) or 'custom'
workflowsDir: 'server/workflows', // where JSON definitions live
exposeApi: true, // register /api/_workflow routes
},
})Defining a Workflow
Create a JSON file in your workflowsDir:
// server/workflows/default.json
{
"name": "default",
"description": "Issue Tracker",
"states": [
{ "slug": "backlog", "name": "Backlog", "color": "#6366F1", "category": "start" },
{ "slug": "todo", "name": "Todo", "color": "#3B82F6", "category": "in-progress" },
{ "slug": "in-progress", "name": "In Progress", "color": "#F59E0B", "category": "in-progress" },
{ "slug": "review", "name": "Review", "color": "#8B5CF6", "category": "in-progress" },
{ "slug": "done", "name": "Done", "color": "#10B981", "category": "end" }
],
"transitions": [
{ "from": "backlog", "to": "todo", "direction": "forward" },
{ "from": "todo", "to": "in-progress", "direction": "forward",
"handler": [{ "name": "permission", "params": { "permission": "start-work" } }] },
{ "from": "in-progress", "to": "review", "direction": "forward" },
{ "from": "review", "to": "done", "direction": "forward",
"handler": [{ "name": "confirm", "params": { "message": "Mark as done?" } }] },
{ "from": "review", "to": "in-progress", "direction": "backward" },
{ "from": "todo", "to": "backlog", "direction": "backward" }
]
}Each state has a category (start, in-progress, or end) that controls behavior — for example, end states have no outgoing transitions.
Using the Composable
useWorkflow is auto-imported and fetches a workflow by name:
<script setup>
const workflow = await useWorkflow('default')
// Get all states
workflow.getStates()
// Get possible next states from a given state
workflow.getNextStates('todo', 'forward')
// => ['in-progress']
// Find a specific transition rule
workflow.findTransition('todo', 'in-progress')
// Execute a transition (runs handler pipeline)
const result = await workflow.transition('todo', 'in-progress', {
issue: currentIssue,
})
// Returns the TransitionRule on success, or false if a handler blocked it
</script>Transition Handlers
Handlers are TypeScript files in app/transitions/ that run during a transition. They can validate, prompt the user, or enrich the transition args.
// app/transitions/confirm.ts
export default defineTransitionHandler({
name: 'confirm',
friendlyName: 'Confirm Action',
description: 'Asks the user to confirm before proceeding.',
run: async (params, _args) => {
const message = (params?.message as string) ?? 'Are you sure?'
if (!window.confirm(message)) {
return false // blocks the transition
}
},
})Handlers are referenced by name in the workflow JSON:
{ "from": "review", "to": "done", "direction": "forward",
"handler": [{ "name": "confirm", "params": { "message": "Mark as done?" } }] }Multiple handlers run in sequence. A handler can:
- Return
falseto block the transition - Return an object to merge data into the args for subsequent handlers
- Return nothing to allow the transition to proceed
Set global: true on a handler to run it on every transition automatically.
Custom Workflow Provider
The built-in file provider reads JSON from disk. For database-backed workflows, use the custom provider:
// nuxt.config.ts
export default defineNuxtConfig({
workflow: {
provider: 'custom',
},
})Then register your provider in a Nitro plugin:
// server/plugins/workflow.ts
export default defineNitroPlugin(() => {
setWorkflowProvider({
async getWorkflow(name) {
return await db.workflows.findOne({ name })
},
async listWorkflows() {
return await db.workflows.findMany()
},
// Optional: enable write operations
async saveWorkflow(workflow) { /* ... */ },
async deleteWorkflow(name) { /* ... */ },
})
})setWorkflowProvider and useWorkflowProvider are auto-imported in server routes.
Typed Conditions
Transitions support a generic conditions bag for domain-specific filtering. Define your conditions shape, then pass a conditionFilter that decides which transitions are available based on runtime context:
interface MyConditions {
role?: string
feature?: string
}
const workflow = await useWorkflow<MyConditions>('default', {
conditionFilter: (rule, context) => {
if (rule.conditions?.role && rule.conditions.role !== context.userRole) return false
if (rule.conditions?.feature && !context.enabledFeatures?.includes(rule.conditions.feature)) return false
return true
},
})
// Only transitions matching the current context are returned
workflow.getNextStates('todo', 'forward', {
userRole: 'admin',
enabledFeatures: ['beta'],
})In the workflow JSON, attach conditions to any transition:
{ "from": "review", "to": "done", "direction": "forward",
"conditions": { "role": "admin" } }API Routes
When exposeApi is enabled (default), two routes are registered:
| Route | Description |
| --- | --- |
| GET /api/_workflow | List all workflows |
| GET /api/_workflow/:name | Get a workflow by name |
Contribution
# Install dependencies
npm install
# Generate type stubs
npm run dev:prepare
# Develop with the playground
npm run dev
# Build the playground
npm run dev:build
# Run ESLint
npm run lint
# Run Vitest
npm run test
npm run test:watch
# Release new version
npm run release