Workflow authoring guide
Workflows in vibe_erp are data, not code. State machines, approval chains, and document routing are declarative, so a new customer is onboarded by editing definitions instead of editing source. This is guardrail #2 in CLAUDE.md, and it shapes everything below.
For the architectural placement of the workflow engine, see ../architecture/overview.md. For the full reasoning, see ../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md.
BPMN 2.0 on embedded Flowable
vibe_erp embeds the Flowable engine and uses BPMN 2.0 as the workflow language. BPMN is a standard, and the standard is the contract: there is no vibe_erp-specific workflow DSL to learn or to invent. Anything a BPMN 2.0 modeling tool can produce, vibe_erp can run.
Flowable runs in-process inside the Spring Boot host. Its tables (flowable_*) live in the same Postgres database as the core PBCs and are untouched by vibe_erp.
Two authoring paths
Both paths are first-class. Tier 1 is preferred whenever it is expressive enough; Tier 2 is the escape hatch.
Tier 1 — Visual designer in the web UI (v1.0)
Business analysts draw workflows in the BPMN designer that ships in the web SPA. The result is stored as a row in metadata__workflow, deployed to Flowable, and tagged source = 'user' so it survives plug-in install/uninstall and core upgrades. No build, no restart, no plug-in.
This path is the right one for:
- Approval chains the customer wants to author themselves.
- Document routing rules that the customer wants to tune themselves.
- Workflows that compose existing typed task handlers in a new order.
The visual designer is a v1.0 deliverable; the current build ships the underlying API only. (Even Flowable itself is not yet wired — see PROGRESS.md for the status of P2.1.)
Tier 2 — .bpmn files in a plug-in JAR
Plug-in authors ship .bpmn files inside their JAR under src/main/resources/workflow/. The plug-in lifecycle deploys them to Flowable on plug-in install. The plugin.yml manifest lists them so the loader picks them up.
src/main/resources/
├── plugin.yml
└── workflow/
├── quote_to_job_card.bpmn
└── reprint_request.bpmn
# plugin.yml
metadata:
workflows:
- workflow/quote_to_job_card.bpmn
- workflow/reprint_request.bpmn
This path is the right one for:
- Workflows that need new typed task handlers shipped alongside them.
- Workflows that the plug-in author wants under version control with the rest of the plug-in code.
- Workflows that ship as part of a vertical-specific plug-in (the printing-shop plug-in is the canonical example).
Service tasks call typed TaskHandler implementations
A BPMN service task in vibe_erp does not embed scripting code. It references a typed TaskHandler by id, and the host routes the call to the matching implementation registered by a plug-in.
The plug-in registers a handler:
@Extension(point = TaskHandler::class)
class ReserveStockHandler : TaskHandler {
override val id: String = "printing.reserve_stock"
override fun handle(task: WorkflowTask) {
val itemId = task.variable<String>("itemId")
?: error("itemId is required")
val quantity = task.variable<Int>("quantity") ?: 0
// ... call into the plug-in's services here ...
task.setVariable("reservationId", reservationId)
task.complete()
}
}
The BPMN service task references the handler by its id (printing.reserve_stock). The host validates at deploy time that every referenced handler id exists and rejects the deployment otherwise — broken workflows fail at install, not at runtime.
There is no scripting language to invent. The TaskHandler is just Kotlin code behind a typed interface, with the same testability, debuggability, and review surface as the rest of the codebase.
User tasks render forms from metadata
A BPMN user task references a form definition by id. The host looks the id up in metadata__form and renders the form using the same code path as Tier 1 forms — there is no parallel form renderer for workflows.
This means:
- The same form can be used inside a workflow user task and outside a workflow.
- A custom field added through Tier 1 customization automatically appears on every workflow user task that uses the same form.
- Validation runs in exactly the same place whether the form is rendered inside a workflow or not.
For details on how forms work, see the form authoring guide.
Worked example: quote-to-job-card
The reference printing-shop plug-in ships a quote_to_job_card.bpmn workflow that exercises every concept in this guide. In rough shape:
-
Start event: a sales clerk creates a quote (user task; uses a form from
metadata__form). -
Service task
printing.price_quote: calls aTaskHandlerregistered by the plug-in to compute the price from the customer's price list and the quote's line items. - Exclusive gateway: routes on whether the quote total exceeds the customer's pre-approved limit.
- User task (manager approval): rendered from a form definition; the manager can approve, reject, or send back for revision.
-
Service task
printing.reserve_stock: anotherTaskHandlerthat reserves raw materials. -
Service task
printing.create_job_card: materializes the approved quote as a production job card (a custom entity defined by the plug-in). -
End event: publishes a
JobCardCreatedDomainEventso other PBCs and plug-ins can react without coupling.
What is worth noticing:
- Every printing-specific concept — quote, price list, job card, reserve stock for plates and inks — lives in the plug-in, never in core PBCs. The core only knows about generic documents, workflows, forms, and events.
- The cross-PBC interaction in step 7 goes through a
DomainEvent, not a direct call. PBCs never import each other. - Steps 4 and 5 can be reordered, removed, or duplicated through the BPMN designer in the web UI without touching plug-in code, because the typed handlers are decoupled from the workflow shape.
Where to go next
- Plug-in author walkthrough including a
TaskHandlerexample:../plugin-author/getting-started.md - Form authoring (the other half of any workflow that has user tasks):
../form-authoring/guide.md - Plug-in API surface, including
api.v1.workflow:../plugin-api/overview.md