Skip to content

Core concepts

What you'll learn: the vocabulary you'll use in every workflow — steps, the run() primitive, memoization and resume, step kinds, interactivity, and run modes.

Steps

A step is a reusable named constant created by a factory. The most common is an agent step from step.define:

ts
import { step, claude } from 'orch'

const PLAN = step.define('plan', {
  agent: claude({ model: 'claude-opus-4-7' }),
  prompt: 'Draft an implementation plan for the requested feature.',
})

The first argument — 'plan' — is the step name, which is also its memoization key. Other step kinds (commit, createWorktree, ask, command) come from their own factories and slug their names automatically; see Step kinds.

The run() primitive

run is the single argument orch passes to your workflow function. It is the only way to invoke a step. It returns the step's typed result:

ts
export default workflow('feature', async (run) => {
  const plan = await run(PLAN, { prompt: 'Plan the auth refactor' })
})

The second argument is a set of per-call overrides:

OverrideEffect
asMemoize this call under a different name. Required when you run() the same step twice.
promptReplace the step's default prompt for this call.
extraContextA JSON value appended to the prompt (serialized).
extraPromptExtra text appended after the prompt.
modeForce 'interactive' or 'autonomous' for this call.
ts
await run(WORK, { as: 'work-auth', prompt: 'Implement the auth module' })
await run(WORK, { as: 'work-api', prompt: 'Implement the API module' })

Without distinct as: names, running the same step twice collides on one memoization key.

Memoization and resume

This is the idea that shapes everything. Every run() call checks state.json for a cached entry under its name:

  • Hit → return the cached value, skip execution.
  • Miss → run the step, persist the result, return it.

So resume re-executes the whole workflow function, but every run() that already completed returns instantly from cache. The first one that didn't finish actually runs.

The practical consequence: code between run() calls runs every time the function executes — including on every resume. Keep it idempotent.

ts
// BAD — appends twice on resume
await run(PLAN)
await fs.appendFile('log.txt', 'planned\n')   // re-runs on every resume
await run(WORK)

Make side effects idempotent, or move them inside a step. See Debugging and the resume CLI command.

Step kinds

A workflow is built from five kinds of step. Each has its own factory.

KindFactoryWhat it does
agentstep.define(name, config)Run an agent (claude(), codex(), or your own runner).
askask({ ... })Prompt the human for input; returns a typed result.
commandcommand(name, { argv, onFailure })Run a shell command as a first-class, streamed, memoized step.
commitcommit(message)Stage everything and create a git commit.
worktreecreateWorktree(branch, { enter })Create a git worktree and optionally switch the workflow's cwd into it.

Full signatures and options live in the Authoring API reference.

Interactivity: autonomous vs interactive steps

Agent steps run in one of two modes:

  • Autonomous (default) — orch streams the agent's NDJSON output, renders a transcript, and waits for the agent to finish on its own. No human at the keyboard.
  • Interactive (mode: 'interactive') — orch spawns the agent's real TUI in a pane; you talk to it directly. Set autoStop: true to have orch close the pane automatically when the agent finishes its turn, so an unattended pipeline doesn't stall. autoStop requires a runner that supports it and is only valid on interactive steps.

Separately, the run-level interactivity axis controls ask() steps:

  • --interactive (default) renders prompts normally.
  • --noninteractive (or ORCH_NONINTERACTIVE=1) resolves each ask() from its declared defaultWhenNoninteractive — for CI and scheduled runs. An ask() with no default errors in this mode.

Run modes

A run mode decides where the views render. orch ships two wired modes (single-pane is reserved for a future version):

ModeWhen it's chosenWhat you see
plainCI, no TTY, or --mode=plainTranscript lines on stdout. --format=json emits one NDJSON envelope per event.
two-paneTTY + tmux ≥ 3.2, or --mode=two-paneA tmux session: left pane = live status of every step, right pane = the active step's transcript or interactive TUI.

Resolution precedence: --mode flag > defaultMode in orch.config.ts > CI → plain > TTY + tmux → two-pane > fallback plain. See the CLI reference for the full table and flags like --no-attach.

Where to go next