Relayflows turns a coding-agent task into steps you can inspect and verify. A flow combines shell commands, model calls, and coding agents with a journal that records what each step did and why it completed.
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." Relayflows 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.
Simple Example Flow
import { flow } from '@relayflows/surface';
export default flow('hello', async (f) => {
const greeting = await f.run('echo "Hello from Relayflows"');
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 reason from a closed set: success, step_failed, canceled, or budget_exceeded. There's no fifth option to guess about.
Both steps above name their own cli and model directly — Ctx.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 it just runs whatever model its resolved CLI defaults to, since there's no flow or project default for that. Without a cli at step, flow, or project level, flows run refuses before anything is journaled — exit 2, REFUSED [invalid_spec].
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.
version: '0.1.0'
name: hello-agent
steps:
- id: greet
type: deterministic
command: 'printf hello'
verification:
type: output_contains
value: hello
- id: edit
type: agent
dependsOn: [greet]
instruction: 'Produce the hello artifact.'
recoveryMode: reset
maxIterations: 2
permissions:
fileGlobs: ['artifacts/**']
accessPreset: readwrite
verification:
type: output_contains
value: agent-okThe exit code is always checked. On top of that, output_contains adds an opt-in string match, 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.
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 Relayflows
○ 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.