Building flows
Draw a flow in the editor, wire branches, joins and loops, validate it and publish a version orders can run on.
A flow is a JSON graph of catalog nodes and the edges between them. The
editor at /flows/:flowId saves it to D1 as a draft. Publishing snapshots the draft as an
immutable, numbered version, and every order pins the version it started on.
Creating a flow
Section titled “Creating a flow”New flow on /flows takes a name, an order type (purchase_request, b2b_order, reorder, subscription, storefront_order) and a
starting point:
| Start | What you get |
|---|---|
| Template | The built-in flow for that order type |
| Blank | One trigger.manual node named start |
| Describe it | A graph drafted by the copilot from your description, laid out automatically |
The flow id is a slug of the name plus a short suffix. A new flow is a draft until you publish it.
The editor
Section titled “The editor”- Palette. Every node type in the catalog, grouped as Triggers, Commerce, Control and AI. Drag one onto the canvas.
- Canvas. React Flow. Each output branch of a node is its own handle; an edge leaves from
the branch it belongs to. A
control.loopnode has a second input,next, for its back-edge. - Inspector. The selected node’s label and config fields, generated from the node’s catalog
entry, plus its per-node
retriesandtimeout. Validation messages appear beside the field. - Copilot. A prompt bar that proposes a change to the whole graph (“add a fraud check after credit”). The proposal shows what it adds and removes; nothing changes until you accept it. See AI nodes and the copilot.
Save stores the draft and returns the current validation errors and warnings. Publish asks for confirmation, then publishes the next version. Test run publishes too when the graph differs from the published version, then starts a sample order on it: the input of the first dashboard scenario for the flow’s order type, or an empty order when there is none.
A flow created from a built-in template offers the template’s current graph when the template has changed since (layout differences don’t count). It is a proposal you review; nothing is published automatically.
Validation
Section titled “Validation”validateGraph (packages/core/src/graph.ts) runs in the editor as you edit, on save, and
before publishing. Publishing is refused while there are errors.
Errors:
- A node id appears twice, or a node’s type isn’t in the catalog.
- A config field is missing when required, or fails its kind’s check (see the node catalog for each field’s kind).
- The flow doesn’t have exactly one trigger, or an edge points into a trigger.
- An edge points at a missing node, or leaves from an output the source node doesn’t have.
- Forward edges form a cycle. Repeating steps needs a Loop node with a
nextback-edge. - A
nextedge targets anything but a Loop node, or comes from outside that loop’s body. - A non-trigger node has no incoming forward edge.
Warnings, which don’t block publishing: a duplicate edge, and a node with outputs but no outgoing edge (“the run ends there”).
How a run moves through the graph
Section titled “How a run moves through the graph”The rules below are the planner’s; the Workflow only makes them durable.
- Branches. A node finishes by choosing its output branches:
true/falsefor a Condition,approved/rejected/escalatedfor an Approval, one route for the Agent router,outfor single-output nodes. Edges on the chosen branches become active; the rest become inactive. - Joins. A node with several inputs runs once every input is resolved and at least one is
active. So a node after an
if/elseruns once, whichever side was taken. - Skips. A node whose inputs are all inactive is skipped (status
skipped), and so is everything downstream that depends only on it. - Parallel work. Every node that is ready at the same time starts in the same batch.
- Loops. A Loop node evaluates
whileeach time it is entered, up tomaxIterations. Onbodyit runs the nodes reachable from itsbodyoutput; the last of them connects back to the loop’snextinput. When the condition is false (or the limit is reached) it takesdone. Each iteration gets new step names (${nodeId}#${run}), so iterations never replay one another. - Continue as new.
control.continue_as_newends the current Workflow instance and starts a new one on the same flow version, with the order and variables carried over. The order keeps its id; itsgenerationgoes up by one. Use it for long-lived loops such as subscriptions, so the step history stays bounded.
Ending a run
Section titled “Ending a run”A control.end node finishes the order with an outcome:
| Outcome | Order status | Notes |
|---|---|---|
completed |
completed |
The normal ending |
declined |
declined |
A policy or approval “no”: not a person cancelling |
cancelled |
cancelled |
Ends the run as cancelled |
failed |
failed, then rolled_back |
Throws NonRetryableError; Workflows runs every registered compensation in reverse order |
reason is a template and becomes the order’s reason. Every End node also sends the matching
order.<outcome> outbound webhook. A run whose remaining nodes
are all skipped, with no End node reached, finishes as completed.
A commerce node that throws after its retries fails the order the same way, without reaching an End node. Only reserve inventory, authorize payment and create shipment register compensations; they release the reservation, void the hold and cancel the shipment.
Expressions and templates
Section titled “Expressions and templates”Condition, Wait (until), Set variables, Loop (while) and Refund (amount) take
JSON Logic expressions. Text fields interpolate {{path}} templates,
for example Approve order {{order.id}}. Both read the run context:
| Path | Holds |
|---|---|
order |
The order: lines, totals, customerId, currency, meta, … as nodes update it |
nodes.<id> |
The output of each node that has run (latest iteration) |
vars |
Run variables: set by Set variables, AI nodes (into), event payloads, and the vars passed when the order starts |
input |
The order’s meta |
now |
When the instance started, epoch ms |
The expressions reference has the operators and the policy rules.
Retries and timeouts
Section titled “Retries and timeouts”Each commerce, AI, Condition and Set node runs as one step.do. Its retry limit is the node’s
retries, else the node type’s default (commerce.charge has 0), else 3, with a 5-second
exponential backoff. Its timeout is the node’s timeout, else "5 minutes". Approval,
Wait-for-event and Wait nodes don’t use these; their waits have their own timeout field.
Waits and timeouts are divided by TIME_SCALE (default 1440, so a day takes a minute); see
Configuration.
Publishing and versions
Section titled “Publishing and versions”Publishing saves the draft, inserts the next row in flow_versions and points the flow at it.
Versions are immutable. An order started through the API runs the latest published version
unless the request names one (version); a running order keeps its version even after you
publish again. Restarting an order from its page starts a new order on the flow’s current
version, with the same customer and lines and without meta.simulate.