Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Workflow graphs

orbit graph renders a workflow as a diagram: a Mermaid flowchart, a Graphviz (DOT) digraph, an SVG image, or a Mermaid sequence diagram of the calls the workflow makes. The diagram is drawn from the compiled definition, so it shows exactly what the runtime executes: implied dependencies, conditions, fan-out, retries, timeouts, compensations, outcome handlers and event handlers.

Usage

orbit graph workflows/order.orbit                      # Mermaid flowchart, to stdout
orbit graph workflows/order.orbit --format dot         # Graphviz DOT
orbit graph workflows/order.orbit --format svg -o order.svg
orbit graph workflows/order.orbit --sequence           # Mermaid sequence diagram
orbit graph workflows/fulfilment.orbit --workflow Shipping
Argument or optionDefaultMeaning
<FILE>requiredA .orbit file or a compiled .json IR file.
--workflow <NAME>see belowThe workflow to render, when the file defines several.
--format <FORMAT>mermaidmermaid, dot or svg.
--sequenceoffRender a Mermaid sequence diagram instead of the step graph. Only with --format mermaid.
-o, --output <PATH>stdoutWrite to a file (parent directories are created) and print wrote <PATH> on stderr.

Without --workflow, a file with one workflow renders that workflow, and a file with a parent workflow and its children renders the parent (the one workflow no other workflow in the file starts). Otherwise the command lists the workflows and asks you to choose:

$ orbit graph examples/fulfilment.orbit --workflow Nope
error: no workflow `Nope`; available: Shipping, Fulfilment

The file must compile; compilation errors are printed as with orbit check, and the command exits with status 1. --sequence with --format dot or svg is an error (status 1).

Deterministic output

The same definition always renders to the same text: steps appear in the IR’s execution order (a stable topological order), dependencies are sorted, handlers appear in the order failure, cancel, timeout, and event blocks are sorted by signal. You can commit the diagram next to the workflow and review changes to it as a diff. The first comment lines carry the workflow name, version and definition hash, so a stale diagram is visible in review.

What the graph shows

ElementMermaidGraphvizSVG
Steprectangle id["..."]rounded boxrounded box
Step with a when conditionhexagon id{{"..."}}shape=hexagonbox with an accent border
Dependency (from after or from reading another step’s output)a --> b"a" -> "b"curved arrow
Compensationstadium <step>__undo(["undo <step>..."]) with class compensation, linked by <step> -. compensate .-> <step>__undodashed box, dashed edge labelled compensatedashed box to the right of the step
Signal feeding an await stepflag signal__<name>>"signal <name>"], dotted edge to the stepshape=cds, dashed edgenot drawn (the step label names the signal)
on failure, on cancel, on timeoutsubgraph on_<kind>["on <kind>"]dashed cluster cluster_on_<kind>labelled panel below the main graph
on event <signal>subgraph on_event_<signal>["on event <signal>"]dashed clusterlabelled panel
continue_as_newsubroutine continue_as_new[["continue as new..."]] after the final steps, and continue_as_new -. next run .-> <first step>peripheries=2 box, dashed next run edge with constraint=falsebox with a double accent border below the final steps, dashed next run edge up the right side into the first steps

Node ids of handler and event steps are prefixed with the subgraph id (on_failure__tell_customer), so a handler step may share a name with a main step.

Each step’s label lists, one per line:

LineWhen presentExample
step namealwayscharge
actionalwayspayments.charge@1.0.0 · python, child workflow Shipping, sleep 1h, await signal review, compute
fan-outfor stepsfor each item (max 4 parallel)
conditionwhenwhen analyze_risk.score < 0.8
retrymore than one attemptretry 5 attempts, exponential 500ms..10s
idempotencyidempotency_keyidempotency key
timeoutstimeout, queue_timeout, await ... timeouttimeout 30s, queue 5m

Conditions are printed back as ORBIT expressions from the IR. Durations inside expressions appear as milliseconds, because the IR does not keep the unit. In Mermaid labels, <, >, " and # are written as the entities #lt;, #gt;, #quot; and #35;, which Mermaid renders as the characters.

The header comment also shows a workflow timeout and a limits block when present (%% workflow timeout 5m, %% limits max_history_events 5000).

Continue-as-new

A workflow with continue_as_new gets one more node. It follows the steps nothing depends on, because the condition is evaluated when every step has finished, and a dashed next run edge goes from it back to the steps without dependencies, where the next run starts. Its label has three lines: continue as new, the condition (when ..., or always without when), and the fields the next run’s input sets (next input { ... }, or the expression when the input is not an object literal). The node id continue_as_new is a keyword, so it never clashes with a step.

$ orbit graph examples/subscription.orbit
flowchart TD
    %% Subscription@1.0.0
    %% sha256:1d7a374edd1e400b4fb61a0d851ea74a99b4620006f6be5f06ce7a9421d6f8f1
    classDef compensation stroke-dasharray: 4 3
    classDef signal stroke-dasharray: 2 2
    charge["charge<br/><small>payments.charge@1.0.0</small><br/><small>retry 5 attempts, exponential 1m..1h</small><br/><small>idempotency key</small>"]
    receipt["receipt<br/><small>notifications.send@1.0.0</small>"]
    charge --> receipt
    next_period["next_period<br/><small>sleep 30d</small>"]
    receipt --> next_period
    continue_as_new[["continue as new<br/><small>when input.period #lt; input.periods</small><br/><small>next input { customer_id, amount, periods, period, charged }</small>"]]
    next_period --> continue_as_new
    continue_as_new -. next run .-> charge
$ orbit graph examples/subscription.orbit --format dot
digraph "Subscription" {
  // Subscription@1.0.0
  // sha256:1d7a374edd1e400b4fb61a0d851ea74a99b4620006f6be5f06ce7a9421d6f8f1
  rankdir=TB;
  node [shape=box, style=rounded, fontname="Helvetica", fontsize=11];
  edge [fontname="Helvetica", fontsize=10];
  "charge" [label="charge\npayments.charge@1.0.0\nretry 5 attempts, exponential 1m..1h\nidempotency key"];
  "receipt" [label="receipt\nnotifications.send@1.0.0"];
  "charge" -> "receipt";
  "next_period" [label="next_period\nsleep 30d"];
  "receipt" -> "next_period";
  "continue_as_new" [label="continue as new\nwhen input.period < input.periods\nnext input { customer_id, amount, periods, period, charged }", peripheries=2];
  "next_period" -> "continue_as_new";
  "continue_as_new" -> "charge" [style=dashed, constraint=false, label="next run"];
}

orbit graph examples/subscription.orbit --format svg:

Step graph of examples/subscription.orbit: charge, receipt and next_period, then continue as new with a dashed next run edge back to charge

In a sequence diagram (--sequence), the workflow sends itself a continue_as_new message after the main steps, inside opt when <condition> when there is a condition:

$ orbit graph examples/subscription.orbit --sequence
sequenceDiagram
    %% Subscription@1.0.0
    %% sha256:1d7a374edd1e400b4fb61a0d851ea74a99b4620006f6be5f06ce7a9421d6f8f1
    participant W as Subscription
    participant payments
    participant notifications
    W->>+payments: charge: payments.charge@1.0.0
    payments-->>-W: PaymentResult
    W->>+notifications: receipt: notifications.send@1.0.0
    notifications-->>-W: { delivered }
    Note over W: next_period: sleep 30d
    opt when input.period < input.periods
        W->>W: continue_as_new: next run with { customer_id, amount, periods, period, charged }
    end

The web UI draws the same node, coloured by whether a run continued.

Mermaid

A real diagram of examples/checkout.orbit:

$ orbit graph examples/checkout.orbit
flowchart TD
    %% Checkout@1.0.0
    %% sha256:8e0eda310a1bac4d68d8910cb6103264f436a98cc16535b7c5e95eac2a4a53c3
    classDef compensation stroke-dasharray: 4 3
    classDef signal stroke-dasharray: 2 2
    reserve["reserve<br/><small>inventory.reserve@1.0.0</small>"]
    reserve__undo(["undo reserve<br/><small>inventory.release@1.0.0</small>"]):::compensation
    reserve -. compensate .-> reserve__undo
    charge["charge<br/><small>payments.charge@1.0.0</small>"]
    reserve --> charge
    subgraph on_failure["on failure"]
        direction TB
        on_failure__tell_customer["tell_customer<br/><small>notifications.send@1.0.0</small>"]
        on_failure__summary["summary<br/><small>compute</small>"]
    end
    subgraph on_cancel["on cancel"]
        direction TB
        on_cancel__confirm_cancel["confirm_cancel<br/><small>notifications.send@1.0.0</small>"]
    end

This documentation is built without a Mermaid plugin, so the block above shows as source. GitHub and GitLab render mermaid code blocks in Markdown files, issues and pull requests. The same workflow rendered by orbit graph examples/checkout.orbit --format svg:

Step graph of examples/checkout.orbit, rendered by orbit graph –format svg

A workflow with conditions, retries and compensations (examples/order_processing.orbit):

flowchart TD
    %% OrderProcessing@1.0.0
    %% sha256:4fdfac082bf3f2fbbbc8ac5465d37cba01dffd1b8e4ae08e25be3e0d76486c41
    %% workflow timeout 5m
    classDef compensation stroke-dasharray: 4 3
    classDef signal stroke-dasharray: 2 2
    validate_order["validate_order<br/><small>orders.validate@1.0.0 · typescript</small><br/><small>timeout 10s</small>"]
    analyze_risk["analyze_risk<br/><small>risk.analyze@1.0.0 · python</small><br/><small>retry 3 attempts, exponential 1s..30s</small>"]
    reserve_inventory["reserve_inventory<br/><small>inventory.reserve@1.0.0 · typescript</small>"]
    validate_order --> reserve_inventory
    reserve_inventory__undo(["undo reserve_inventory<br/><small>inventory.release@1.0.0</small>"]):::compensation
    reserve_inventory -. compensate .-> reserve_inventory__undo
    process_payment{{"process_payment<br/><small>payments.charge@1.0.0 · python</small><br/><small>when analyze_risk.score #lt; 0.8</small><br/><small>retry 5 attempts, exponential 500ms..10s</small><br/><small>idempotency key</small><br/><small>timeout 30s</small>"}}
    analyze_risk --> process_payment
    reserve_inventory --> process_payment
    process_payment__undo(["undo process_payment<br/><small>payments.refund@1.0.0</small>"]):::compensation
    process_payment -. compensate .-> process_payment__undo
    send_notification["send_notification<br/><small>notifications.send@1.0.0 · typescript</small>"]
    process_payment --> send_notification

process_payment depends on analyze_risk because its condition reads analyze_risk.score, even though its after names only reserve_inventory.

A for step and a child workflow (examples/fulfilment.orbit):

$ orbit graph examples/fulfilment.orbit --workflow Fulfilment
flowchart TD
    %% Fulfilment@1.0.0
    %% sha256:ecffc60c3bd361d5112497749c532dc808cbe2db9f35af22c1ffee8f2f9d90d3
    classDef compensation stroke-dasharray: 4 3
    classDef signal stroke-dasharray: 2 2
    reserve["reserve<br/><small>inventory.reserve@1.0.0 · typescript</small><br/><small>for each item (max 4 parallel)</small>"]
    reserve__undo(["undo reserve<br/><small>inventory.release@1.0.0</small>"]):::compensation
    reserve -. compensate .-> reserve__undo
    ship["ship<br/><small>child workflow Shipping</small><br/><small>timeout 1h</small>"]

The child workflow is a single node. Render it separately with --workflow Shipping.

To produce an image from Mermaid source, use the Mermaid CLI:

orbit graph workflows/order.orbit -o order.mmd
npx -y @mermaid-js/mermaid-cli -i order.mmd -o order.svg

Graphviz

$ orbit graph examples/shipment.orbit --format dot
digraph "Shipment" {
  // Shipment@1.0.0
  // sha256:dc7d17291b8a4c9bcea44a73e12a8bbde9f20ec9d36f5447ea90d42882f4154d
  // limits max_history_events 5000
  rankdir=TB;
  node [shape=box, style=rounded, fontname="Helvetica", fontsize=11];
  edge [fontname="Helvetica", fontsize=10];
  "signal__delivered" [label="signal delivered", shape=cds, style=dashed];
  "arrival" [label="arrival\nawait signal delivered\ntimeout 30d"];
  "signal__delivered" -> "arrival" [style=dashed];
  "confirm" [label="confirm\nnotifications.send@1.0.0"];
  "arrival" -> "confirm";
  subgraph "cluster_on_event_location" {
    label="on event location";
    style=dashed;
    "on_event_location__text" [label="text\ncompute"];
    "on_event_location__tell" [label="tell\nnotifications.send@1.0.0"];
    "on_event_location__text" -> "on_event_location__tell";
  }
}

Render it with Graphviz:

orbit graph workflows/order.orbit --format dot | dot -Tsvg -o order.svg
orbit graph workflows/order.orbit --format dot | dot -Tpng -o order.png

SVG without external tools

--format svg lays the graph out itself and writes a standalone SVG document; neither Graphviz nor a browser is needed. The layout is simple and deterministic:

  • each step is placed one layer below its deepest dependency (longest path from the steps without dependencies), and steps in a layer keep the execution order;
  • a compensation is placed to the right of the step it undoes, on the same layer;
  • each outcome handler and event block is laid out the same way in its own labelled panel, below the main graph;
  • a continue_as_new node is placed one layer below the final steps; its dashed next run edges run up the right side of the graph and into the top of the first steps;
  • dependencies are curves from the bottom of a step to the top of its dependent. An edge that skips a layer can pass behind a node of the layer in between; use Graphviz for large graphs where that matters.

The SVG uses a monospace font, embeds its own stylesheet (colours follow prefers-color-scheme) and has a transparent background. orbit docs --format html embeds the same rendering in each workflow page.

Sequence diagrams

--sequence renders the calls a workflow makes as a Mermaid sequence diagram:

$ orbit graph examples/approval.orbit --sequence
sequenceDiagram
    %% OrderApproval@1.0.0
    %% sha256:939fd564b818da34796032e2c0086ce2f28118e2e9f31a6d9c1b21fdbfc57066
    participant W as OrderApproval
    participant Sender
    participant payments
    participant notifications
    Note over W: assess: compute
    opt when assess.needs_approval
        Sender->>W: decision: signal review (timeout 2d)
    end
    opt when !assess.needs_approval || coalesce(decision.approved, false)
        W->>+payments: charge: payments.charge@1.0.0
        payments-->>-W: PaymentResult
    end
    W->>+notifications: notify: notifications.send@1.0.0
    notifications-->>-W: { delivered }
    opt compensation, in reverse order, after a failure or cancellation
        W->>+payments: undo charge: payments.refund@1.0.0
        payments-->>-W: { refunded }
    end
ElementDrawn as
Worker taska call to a participant named after the task’s namespace (payments for payments.charge) and a reply with the contract’s output type (a record’s name, or its field names)
Child workflowa call to the participant child_<Name>
awaita message from the participant Sender
sleep, computea note over the workflow
whenan opt when <condition> block
fora loop for each <variable> block
Compensationsone opt compensation ... block at the end, in reverse execution order
continue_as_newa message W->>W: continue_as_new: next run with { ... } after the main steps, in an opt when <condition> block when it has one
Handlers and event blocksone opt on <kind> or opt on event <signal> block each

Steps appear in execution order. Steps that can run concurrently are drawn one after the other; the flowchart shows which steps are independent.

In VS Code

ORBIT: Show Step Graph (Mermaid) runs orbit graph on the current file and opens the Mermaid source beside the editor. See Editor support.

See also