A step is a node in a business process. Beyond the plain task step (does work, requires a capability, may present a ui), a step can take a control-flow type that shapes how the flow moves through it. One flow — Chowk's FulfilOrder — exercises the whole vocabulary:
business_process FulfilOrder {
title "Fulfil a paid order"
actor platform.v1.System
subject ordering.v1.Order
trigger event ordering.v1.OrderPaid
goal "A paid order is reserved against stock, packed, and shipped — with the customer notified in parallel."
step reserve call ReserveStock {
on success -> route
on error -> end "out-of-stock"
}
step route gateway {
when "subject.status == 'CANCELLED'" -> end "cancelled"
otherwise -> dispatch
}
step dispatch fork {
fork -> pack
fork -> notify
}
step pack by platform.v1.Operator {
requires [fulfillment.v1.ShipmentFulfillment]
timeout "PT24H"
on success -> settle
on error -> end "fulfilment-failed"
}
step notify {
on success -> settle
}
step settle join {
join [pack, notify]
on success -> end "shipped"
}
}Rendered, that flow reads:
flowchart TD
reserve["reserve · call ReserveStock"] -->|success| route
reserve -->|error| oos([out-of-stock])
route{"route · gateway"} -->|"status = CANCELLED"| cx([cancelled])
route -->|otherwise| dispatch["dispatch · fork"]
dispatch --> pack["pack · by Operator"]
dispatch --> notify["notify"]
pack --> settle{{"settle · join"}}
notify --> settle
settle --> shipped([shipped])The step types
| Type | Role |
|---|---|
| (task) | the default — does work; requires capabilities, may present ui |
gateway | an exclusive (XOR) decision — routes to exactly one target by evaluating its when guards top to bottom, falling through to otherwise |
fork | a parallel split — activates all of its fork edges at once |
join | a parallel join — waits for the named branches (join [pack, notify]) to complete, then continues |
call | invokes a sub-process by name (call ReserveStock) — composition inside one flow |
wait | pauses for a timer or event, bounded by timeout |
Transitions and terminals
A step's outgoing edges are conditioned on its outcome:
on success -> <step>/on error -> <step>— the two task outcomes.when "<expr>" -> <step>— a guarded edge, the primary form on agateway. When the flow declares a typedsubject, the guard reads real fields (subject.status == 'CANCELLED') and is type-checked against them.otherwise -> <step>— the fall-through arm of a gateway.fork -> <step>— an unconditional parallel-split edge from aforknode.
Any target is either another step name or end "<label>" — a labelled terminal. Labelled ends are how a flow names its distinct outcomes ("shipped", "out-of-stock", "cancelled").
Swimlanes: by <Actor>
FulfilOrder runs as the System actor, but the physical pack step is performed by an Operator — step pack by platform.v1.Operator. A change of actor between steps is a handoff, rendered as a swimlane boundary. It is how one flow crosses personas without splitting into two.
Iteration: repeat over
The reserve step calls ReserveStock, a sub-process that reserves each order line. That per-element repetition is a repeat over marker:
business_process ReserveStock {
title "Reserve stock for an order"
actor platform.v1.System
subject ordering.v1.Order
trigger call FulfilOrder
step reserve {
requires [inventory.v1.StockReservation]
repeat over "subject.lines" as line
}
}repeat over marks a step as running once per element of a collection — distinct from a flow back-edge (a transition pointing at an earlier step), which repeats the flow. There is no loop keyword; looping is one of those two things.
Client-resolved branches: decision
Not every branch is a server gateway. A decision is a branch the actor or client resolves — a choice with no backend operation behind it:
step review "Review the delivered order" {
requires [OrderTracking]
ui CheckoutScreen
decision "keep it, or return it?"
when "intent == 'return'" -> request_return
otherwise -> end "kept"
}The guard here reads the actor's choice, not a server outcome — the flow branches on what the person decides.