Skip to content

Authoring a BPMN diagram

BPMN Flow has one editor: a canvas built on Svelte Flow, reached the same way as DAG Flow and Code Flow — from the Create flow modal. There is no editor chooser and nothing to opt into; every BPMN process, new or imported, opens on this canvas.

This page covers how to build a diagram. For what each element actually does once deployed, see the element reference and execution semantics; for the tier this editor requires, see BPMN Flow overview.

The Run button in the toolbar starts a process instance from variables you fill in — and it does this against the diagram as it currently sits in the editor, not the last deployed version. Nothing is written to the deployed process definition; a test run has no side effect on it. This is deliberate: a Script or DAG Flow’s own “Run” already tests the current unsaved buffer, and BPMN Flow’s Run button matches that behaviour rather than forcing a deploy first.

If the process has been deployed at least once, Run shows a typed form derived from the diagram’s declared start variables. A process that has never been deployed — including a brand-new one — falls back to a JSON editor for the same purpose: there’s no schema to build a form from yet. Either way, Run is the loop you’ll use while building: drop elements, wire them up, Run to check behaviour, then Save draft or Deploy once it’s right.

The left rail lists every element the editor can author, grouped and filterable by name:

GroupElements
EventsStart, Timer Start, End, Error End, Timer Catch, Message Catch, Throw, Error Boundary
TasksTask, Script Task, Service Task, User Task, Business Rule Task, Manual Task
StructureSub Process
GatewaysExclusive Gateway, Parallel Gateway, Inclusive Gateway

Drag an entry onto the canvas, or click it to drop it in the centre of the current view. This list is deliberately identical to the engine’s supported-element set (see the element reference for what each one does at runtime) — there is no palette entry for a construct the engine can’t execute. The one exception worth calling out: Manual Task has no special engine behaviour of its own and runs as a plain pass-through step, the same as an untyped Task.

The canvas snaps to a 20-unit grid. Dropping or adding an element centres it under the cursor first, then snaps its top-left corner to the grid — so a dropped shape lands where you aimed rather than shifted toward the nearest grid line. Arrow keys nudge the current selection by one grid step; holding Shift nudges by five steps at once, for faster coarse positioning without a mouse. Loading an existing file never re-snaps anything — that would dirty a diagram you only opened, before you’ve changed a thing.

Drag from one element’s connection handle to another to create a sequence flow. A connection is refused, mid-drag, in two situations the engine would otherwise only reject at deploy time:

  • Arity. Each element type has a fixed rule for how many incoming and outgoing flows it may carry: a start event takes no incoming (it’s the entry), an end event emits no outgoing (it’s the exit), a boundary event takes no incoming but emits one outgoing, and a plain task accepts only one outgoing flow — a second one would be an implicit parallel fork, which the validator rejects. Gateways are the only elements that branch or merge, so both directions are unbounded there.
  • Scope. A sequence flow can’t cross a sub-process boundary — its source and target must sit in the same scope. Flows into or out of the sub-process element itself are fine; it’s a flow that tries to reach inside another scope’s contents that’s refused.

Both rules are enforced by the same check the engine’s validator implements, just moved earlier — so an invalid connection simply doesn’t complete, rather than deploying and failing later with a less specific error.

Every element carries a Change type control (on the inspector’s Advanced tab — see below), which swaps its BPMN type while keeping its id, so every connected flow, and every DI reference, survives the change unbroken.

Not every change is free, and not every change is legal:

  • A refused change — one the target type genuinely can’t represent given the element’s current connections or attachment — shows an inline reason, never a code, and doesn’t apply.
  • A lossy but legal change opens a confirmation naming exactly what’s lost, in plain language — “Clears the bound script u/admin/prep”, never “some attributes may be lost.” Typical losses: a script/decision binding that only survives between compatible task types (script ⇄ service task keeps its script; anything else clears it), an event definition being replaced outright (a timer’s cron is lost if you retype away from a timer), or a boundary event’s attachment being cleared if you retype it away from a boundary type.
  • A lossless change — for example exclusive ⇄ parallel gateway with no bound script or decision to lose — applies immediately, no confirmation needed.

Drop Error Boundary onto a task or sub-process — nowhere else. An error boundary event with no host is XML the engine rejects at deploy (UnresolvableBoundaryAttachment), so the editor refuses the drop outright with a toast telling you to drag it onto an activity instead. Once attached, the event rides the host’s border rather than sitting on an arbitrary point: it slides along the perimeter as you drag it near the edge, and snaps to the nearest border point if you drop it inside or outside the host. Dragging the host itself carries every boundary event attached to it along, re-projected onto the moved border automatically.

Drag a boundary event far enough outside its host and it detaches, looking for a new host under the drop point; if none is found, it snaps back to its previous host rather than becoming an orphaned event the backend would reject.

Drop any element inside a Sub Process shape and it becomes a child of that scope — contained for real, not just visually overlapping. Its position becomes relative to the sub-process’s own top-left, which is also what the exported BPMN 2.0 XML reflects; dragging the sub-process moves every element inside it along with it. This is what makes sub-process containment mean something at deploy time, rather than just being a picture that happens to look nested.

The right-hand panel changes shape based on what’s selected:

  • Nothing selected shows the Process tab: the process id, the diagram’s <bpmn:error>/<bpmn:message> registry (add, edit, and remove entries here — these are what a boundary error event’s or message catch event’s reference field points at), and a rollup of every element the last failed deploy marked.
  • One element selected shows a header (name, id, and a “rejected on deploy” alert if this element was flagged) plus tabs:
    • General — name and free-text documentation.
    • Behaviour — whatever’s specific to this element’s type: the timer field for a timer start event (see below), a script picker for a script/service task, a decision picker for a business rule task, an error selector for an error boundary/end event, a message selector for a message catch event. An element with none of these shows a plain “nothing to configure” note rather than an empty tab.
    • Inputs — only shown for script and service tasks. Arguments passed to the bound script when the task runs, addressable the same way a DAG Flow script step’s inputs are (flow_input, variables, results). If this element was imported with a Camunda/Flowable I/O mapping into the same argument name, an argument set here always wins that collision.
    • Advanced — the element-type control described above, plus, for an attached boundary event, a note on what it’s attached to.
  • One sequence flow selected shows a flat three-field panel instead of tabs — a label, a JavaScript condition expression (addressable roots: flow_input, variables, results, previous_result), and a default-flow toggle.
  • Two or more elements selected shows just a count. Bulk actions on a multi-selection are align/distribute, copy/paste, and delete (see below) — not per-element editing.

A Timer Start event only schedules a process if it carries a root-scope timeCycle: a cron expression, evaluated in UTC, at the top level of the process — not inside a sub-process. The Behaviour tab’s cron field validates what you type live, against the same rule the deploy-time scheduler enforces, and shows the next computed run time once it’s valid.

Three things this field is explicit about rather than silently accepting:

  • A timer start inside a sub-process can’t be scheduled at all, regardless of what you put in the field — a sub-process is instantiated by its parent’s token, not by its own timer. Move the element to the top-level process, or use a plain start event there instead. The field shows this explanation with no cron input, since no cron text would make it schedulable.
  • timeDate and timeDuration are not authorable here. An element imported with one of these shows what it actually is and that it will fail deploy, without silently discarding it — only typing a cron in the field replaces it.
  • An ISO-8601 repeating interval (R.../...) is likewise not what this field writes or accepts — only a timeCycle cron.

All three of the above deploy as errors if left as-is; the field’s job is to say so plainly rather than let you find out at deploy time.

Align, distribute, copy/paste, duplicate, and auto-layout

Section titled “Align, distribute, copy/paste, duplicate, and auto-layout”

Select two or more elements and an align panel appears at the bottom of the canvas: align left/right, top/bottom, or centered on either axis, computed from the selection’s own bounding box rather than a fixed “anchor” element. At three or more selected, distribute (horizontal or vertical, evenly spacing every interior element between the two extremes) also appears.

Copy (⌘C/Ctrl+C), paste (⌘V/Ctrl+V), and duplicate (⌘D/Ctrl+D) work on an internal clipboard, not the system clipboard. Where a paste lands depends on what’s currently selected: a single sub-process selected pastes the fragment inside it; any other single element selected pastes it as a sibling; nothing or multiple elements selected pastes at the process root. Duplicate is different on purpose — it always keeps each duplicated element’s own original parent, so duplicating a sub-process doesn’t nest the copy inside itself.

Auto-layout (the toolbar button that looks like connected nodes) re-lays out the entire diagram left-to-right and resizes every sub-process to fit its contents. It’s never run automatically — only on demand, behind a confirmation dialog — because running it on an imported diagram would discard whatever hand-tuned layout it arrived with. Undo (⌘Z) reverses it like any other edit.

ShortcutAction
⌘Z / Ctrl+ZUndo
⇧⌘Z / Ctrl+Shift+Z (or Ctrl+Y)Redo
⌘A / Ctrl+ASelect all
⌘C / Ctrl+CCopy selection
⌘V / Ctrl+VPaste
⌘D / Ctrl+DDuplicate selection
Delete / BackspaceDelete selection (cascading — see below)
EscapeClear selection
Arrow keysNudge selection by one grid step
+ arrow keysNudge selection by five grid steps

A shortcut never fires while you’re typing in a text field. Deleting an element cascades: removing a task also removes any boundary event attached to it, removing a sub-process also removes everything inside it, and removing any element removes its incident sequence flows — the same cascade whether you press Delete, use the toolbar’s delete button, or delete a multi-selection.

The toolbar’s import/export icons round-trip BPMN 2.0 XML directly — export downloads the current in-editor diagram as a .bpmn file; import replaces what’s loaded with a chosen file (resetting undo history, since it’s a new document). See Importing from Flowable/Camunda for what happens to constructs this editor can’t author when you import them — they’re kept as read-only, carry-through elements rather than dropped.

Every element’s Behaviour tab carries an Async toggle. Turning it on marks the element flowable:async="true" — a forced quiescence boundary the engine honours before the element runs (see Execution semantics) — and an element carrying it shows a small lightning-bolt badge on the canvas. Turning the toggle off removes the attribute from the exported XML entirely, rather than writing flowable:async="false", matching Flowable’s own asymmetric writer. The toggle is available on every element type — tasks, gateways, events, and sub-processes alike — not only activities.

flowable:asyncLeave/asyncAfter and flowable:exclusive have no editor control yet. The engine honours both (see Execution semantics), but today they only survive on an element that already carried them from an import — there’s no toggle to set or clear them from the canvas.

One thing worth being explicit about, since it’s easy to expect from other BPMN tools: there’s no quick-add or canvas search. Every element goes on the canvas via the palette drag or click — there’s no hover-to-append gesture from an existing element, and no way to search across the diagram for an element by name. Both are tracked as future work, not shipped today.