Skip to main content
These practices keep your workflows reliable at runtime and legible on the canvas. A workflow is a committed graph of steps (.then(), .parallel(), .branch(), .foreach(), …), and the canvas is drawn from that graph: every step becomes a node (its kind — action, agent, LLM, hook, wait, sub-workflow — comes from the step itself), and run history lights up each node by its step id. Nothing is parsed from your source, so any valid TypeScript renders correctly; the habits below are about making the graph clear and the steps safe to retry.

The one principle

Put durable work in steps, and compose those steps with the builder. Everything else on this page follows from it. A step is an action, a createStep(), or a helper step (agentStep, llmStep, hookStep, workflowStep, .sleep()). Each step’s result is recorded: when a run is retried or resumed, completed steps keep their results and execution continues from the step that failed or was in flight. A step that fails is re-run from its start, so the work inside it should be safe to repeat. See Durability and retries for the runtime side of this.

Prefer actions as steps

Chain actions directly with .then(action) instead of calling them from inside a custom step. Each chained action is its own recorded step, its own retry boundary, its own credential binding, and its own node on the canvas.
Reach for createStep() when a step needs logic of its own (formatting, a decision, a call that isn’t worth an action), or when you need try/catch around an action to handle its failure yourself.

Keep step ids stable and unique

A step’s id is its identity: run events, retries, credential assignments (step:<id>), and canvas nodes are all keyed by it. Action steps use the action slug; helper steps use the id you pass (agentStep and workflowStep default to the agent or child slug).
  • Reuse an action under a new id with .as("id"). The same action used twice in one workflow needs distinct ids.
  • Set id on agentStep / workflowStep when the same agent or child appears more than once, e.g. in .parallel(). The canvas then names the node by that id (a step keyed by its slug shows the agent, action or workflow name).
  • Don’t rename ids casually. Renaming a step detaches the credential assignments bound to it and changes the keys of .parallel() / .branch() outputs.

Reshape with .map(), keep it pure

Use .map() to adapt one step’s output to the next step’s input, and to build the final output. Its result is recorded (map-<n>, or the { id } you pass so the canvas shows a readable name), but it has no retries and no credentials. Keep it pure — no network calls, writes, clock reads, or random ids. Anything with a side effect belongs in a step.
Branch conditions (.branch(), .dowhile(), .dountil()) follow the same rule: decide from inputData, getInitData(), and getStepResult(), not from outside state.

Make side effects idempotent

A failed step is retried from its start (per retries, and again when the queue retries the run). Design actions so running them twice with the same input is harmless: upsert instead of insert, pass idempotency keys to APIs that support them, and check before creating. Wrap API calls in actions (defineAction) instead of calling fetch in a custom step. Actions get typed I/O, credential resolution, a trace span, and a named node on the canvas.

Use the builder for structure

Control flow written with builder methods shows up as real structure on the canvas and in run history; control flow hidden inside one execute function shows up as a single step.

Reuse workflows with workflowStep

To share orchestration between workflows, import the child and run it with workflowStep(child). It runs as its own tracked child run with its own history and durable waits, and the parent waits for it. Share pure logic (formatting, date math, prompt building) as ordinary functions in src/lib/**.

Let .commit() check the output

.commit() is a type error when the last step’s output does not match the declared output. Fix it with a final .map() that builds exactly the output shape, rather than loosening the output schema.

Quick checklist

  • Each action is its own step (.then(action)), not hidden inside a custom step.
  • Step ids are unique (.as("id"), id on helper steps) and stable.
  • .map() and conditions are pure; side effects live in steps.
  • Side-effecting steps are idempotent and safe to retry.
  • Branches, loops, parallelism, and waits use builder methods.
  • Shared orchestration is a sub-workflow (workflowStep); shared logic is a pure function.

Next steps

Build workflows

Define workflows and compose actions, agents, and durable steps.

Test workflows

Run workflows in tests and assert their output.

Run workflows

Start runs from the CLI, triggers, the API, and agent tools.

Workflow runs

Inspect input, output, steps, errors, and traces.