Flows

Stop babysitting agents. Script them — steps you can inspect, verify, and resume.

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.

Don't want to install anything yet? Try Flows at agentrelay.com/cloud first — no local setup.

The ladder

Every flow is built from the same small set of rungs, and you only climb as high as the task needs:

  1. run — a shell command. No model involved.
  2. llm — a bare model call. Prompt in, verified output out, no workspace, no tool use.
  3. agent — a harnessed coding agent in a workspace. Returns an artifact, not just text.
  4. 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 with flows 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). dispatch is declared and passes flows 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: success

This 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

Don't start from a blank file — the cookbook has verified, copy-paste recipes for the automations people build first.

See how far the ladder goes: named agents, cloud dispatch, memory, and integrations.