Stop babysitting agents. Script them.
Define complex sequences of tasks for agents instead of hoping they follow the rules in your prompt. A flow combines shell commands, model calls, and coding agents with a journal that records what each step did and why it completed — predictable, auditable, and dependable.
Every effect is journaled before it's treated as real. If a step's journal write fails, the step fails. There's no silent fallback and no "it probably worked." Flows makes that trade everywhere: less magic, more evidence.
The ladder
Every flow is built from the same small set of rungs, and you only climb as high as the task needs:
run— a shell command. No model involved.llm— a bare model call. Prompt in, verified output out, no workspace, no tool use.agent— a harnessed coding agent in a workspace. Returns an artifact, not just text.human/dispatch/done— durable approval, handing work to a child flow, and a typed finish.f.human(question, { to })parks the run until a person answers — locally withflows answer, on Cloud by replying yes or no in the Slack thread or on the issue the question was delivered to (2.0.18+; see Human gates).dispatchis declared and passesflows check, but the runtime still refuses it (unsupported_verb).
Simple Example Flow
import { flow } from '@relayflows/surface';
export default flow('hello', async (f) => {
const greeting = await f.run('echo "Hello from Flows"');
console.log(greeting.trim());
const answer = await f.agent('greeter', {
task: 'Reply with one short hello sentence. Do not use tools or modify files.',
cli: 'claude',
model: 'claude-sonnet-4-6',
});
console.log(answer.summary);
f.done('success');
});f.run (a deterministic step in YAML) executes a shell command and returns its output. f.agent (an agent step) hands a task to a coding agent and returns a summary rather than a raw transcript. f.done finishes the run with one verdict from a closed set: success, step_failed, needs_human, or declined. There's no fifth option to guess about — canceled and budget_exceeded exist in the journal vocabulary, but only the kernel records them.
Both steps above name their own cli and model directly — f.agent's options are { task, workspace?, cli?, model? }, matching the YAML step's fields (flows#310). Neither is required: omit cli and a step falls back to its flow's cli, then the nearest flows.json's project-wide default; omit model and Claude runs its adapter default (claude-opus-5) while Codex picks its own. Without a cli at step, flow, or project level, flows check and flows run refuse before anything is journaled — exit 2, REFUSED [cli_unresolved]. If that flows.json also lists models, every model a step names must be on the list.
Verification
An agent rarely fails by crashing. It fails by returning something plausible and wrong, which a plain retry-on-error never catches. So a step's completion is decided by a check the kernel runs against the real output, not by the agent's own account of what happened.
import { flow } from '@relayflows/surface';
export default flow('hello-agent', async (f) => {
await f.run('printf hello')
.gate({ type: 'regex_match', pattern: '^hello' });
await f.agent('edit', {
task: 'Produce the hello artifact and print agent-ok when it exists.',
cli: 'claude',
}).gate({ type: 'regex_match', pattern: 'agent-ok' });
f.done('success');
});The exit code is always checked. On top of that, output_contains (YAML) or a postfix .gate({ … }) (TypeScript) adds an opt-in check against the real output, and an llm step can require its output to match a json_schema instead. A step that fails verification is recorded as verification_failed in the journal — it doesn't get to report success on its own say.
recoveryMode: reset governs what happens if this step dies mid-edit: the next attempt starts over from the pinned workspace revision instead of picking up whatever half-finished state got left behind. permissions limits what that attempt is allowed to touch while it runs. Both are declared per step in YAML today; see Build a flow.
Resumable by construction
Kill the process mid-run and start it again. Steps that already completed don't run a second time, and their effects don't get resent.
$ npx --no-install flows run hello.flow.ts --local-agent --input '{}'
○ run-1 (deterministic) 0.00s
✓ run-1 (deterministic) 1.02s completionReason: success
Hello from Flows
○ agent-2 (agent) [agent: preparing] 0.00s
↻ agent-2 (agent) [agent: running] 12.51s
✓ agent-2 (agent) [agent: completed] 28.95s completionReason: success
Hello! How can I help you today?
RUN 01M20JYM4T9FNGNFX4NQCK34JK completed (3 steps) completionReason: successThis is a real run, captured directly from the terminal. Every step carries a completionReason from a closed, nine-value vocabulary, so the journal always says exactly which one applied — no raw stack trace has to stand in for an answer.
Start here
Quickstart
From nothing to a resumed run in a few commands.
Build a flow
TypeScript or YAML — the same journal underneath.
CLI
check, run, resume, observe.