guide.md 6.57 KB

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:

  1. Start event: a sales clerk creates a quote (user task; uses a form from metadata__form).
  2. Service task printing.price_quote: calls a TaskHandler registered by the plug-in to compute the price from the customer's price list and the quote's line items.
  3. Exclusive gateway: routes on whether the quote total exceeds the customer's pre-approved limit.
  4. User task (manager approval): rendered from a form definition; the manager can approve, reject, or send back for revision.
  5. Service task printing.reserve_stock: another TaskHandler that reserves raw materials.
  6. Service task printing.create_job_card: materializes the approved quote as a production job card (a custom entity defined by the plug-in).
  7. End event: publishes a JobCardCreated DomainEvent so 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