# Flow agents (/docs/build/flows)



A flow agent is an `Agent` whose body is a flow: your code, not the model,
decides what runs next. Its stages run agents, tools, and nested flows. It is
exported, saved, opened as a session, and observed exactly like a ReAct agent.

```ts title="agents/report/agent.ts"
import { Agent, tool } from "@nylorun/agents";
import { z } from "zod";

const researcher = Agent({ id: "researcher", description: "Gathers concise findings." })
  .instructions("Research the topic and return { findings }.")
  .output(z.object({ findings: z.string() }));

const publish = tool({
  name: "publish",
  description: "Records finished findings.",
  input: z.object({ findings: z.string() }),
  output: z.object({ published: z.boolean() }),
  async run() {
    return { published: true };
  },
});

export const report = Agent({ id: "report", name: "Report" })
  .step(researcher)
  .step(publish, { input: ({ input }) => ({ findings: input.findings }) });
```

Export it from the registry exactly as you export an agent:

```ts title="agents/index.ts"
import { report } from "./report/agent.js";

export const agents = [report];
```

## Stages [#stages]

Every stage has the same shape, `.stage(whatRuns, { how })`, and every stage
accepts `id` and `input`.

| Stage                                                           | Behavior                                                                   |
| --------------------------------------------------------------- | -------------------------------------------------------------------------- |
| [`.step(x, { id?, input? })`](/docs/build/step)                 | Runs one agent, tool, flow agent or `flow()`; its output is the next input |
| [`.switch({ ...cases, default? }, { on })`](/docs/build/switch) | `on` returns a case name; exactly that case runs                           |
| [`.parallel(branches)`](/docs/build/parallel)                   | Runs named branches at once on the same input; output is keyed by branch   |
| [`.map(each)`](/docs/build/map)                                 | Runs `each` once per item of its input, which must be a list               |
| [`.loop(body, { verify, max?, decide? })`](/docs/build/loop)    | Runs, verifies, and retries with the feedback until it passes              |

A flow agent has no model, so ReAct methods such as `.instructions()` and
`.tools()` are not on it (`flow.no-model`). An agent is either a ReAct loop or a
flow; mixing them fails the build (`agent.mixed-body`). Put the ReAct part in
its own `Agent` and add it with `.step()`.

## Stage functions [#stage-functions]

Every function a stage takes receives one object:

| Field       | What it holds                                           |
| ----------- | ------------------------------------------------------- |
| `input`     | What this stage received: the previous stage's output   |
| `results`   | Earlier steps' outputs in the same sequence, by step id |
| `flowInput` | The input of the nearest flow agent                     |

The `input` option computes what a stage receives:

```ts
.step(analyst, {
  input: ({ results, flowInput }) => `Summarize ${results.researcher.findings} for ${flowInput.audience}`,
})
```

The functions are `input` on any stage, `on` on a Switch, and `verify` and
`decide` on a Loop. They run as durable executor actions, so keep them
deterministic: derive the result from their arguments only.

Types flow through the stages: each step's `.output()` schema types the next
stage's `input` and `results.<id>`. A Switch without a `default` case must
return a case name, and `.map()` needs a list input, or an `input` function
that returns one.

## Sequences with `flow()` [#sequences-with-flow]

A case, branch, map item or loop body is often more than one step. Build it with
`flow()`, which has the same stage methods and no id:

```ts
import { Agent, flow } from "@nylorun/agents";

export const support = Agent({ id: "support" })
  .step(triage)
  .switch(
    {
      bug: flow().step(fixer).step(runTests),
      default: general,
    },
    { on: ({ input }) => input.kind },
  );
```

A `.step(flow())` with no options is inlined into its parent sequence.

## Sessions and ids [#sessions-and-ids]

Each agent in a flow runs in its own session, linked from the flow agent's
session and named by the agent:

* its id, or the `id` you give its step: `.step(writer, { id: "final-writer" })`
* with `[i]` for each Map item it runs in: `implementer[0]`, `implementer[1]`
* under a nested flow agent's id: `review/reader`

Control stages add nothing, so wrapping a step in a Loop or moving it out of a
Switch keeps its session and transcript. An agent may appear once per flow agent,
even in different Switch cases; use it again under a new id
(`flow.duplicate-leaf`). `.withId()` names a child where there is no options
object: `.parallel({ a: reviewer, b: reviewer.withId("reviewer-b") })`.

A stage's `id` also names its output in `results`, and the key its functions are
bound under. A stage without an id is keyed by its position, such as
`@1.default.1`. A running flow stays pinned to the manifest it started with, and
an executor only runs a flow's functions for the manifest it serves, so name the
stages you expect to reorder.

## Nest flow agents [#nest-flow-agents]

A flow agent can be a step of another flow agent, or a
[subagent](/docs/build/subagents#flow-agents-as-subagents) of a ReAct agent:

```ts
const review = Agent({ id: "review" }).step(reader).step(checker);

export const ship = Agent({ id: "ship" }).step(coder).step(review).step(openPr);
```

A nested flow agent runs in the parent flow's session, with its own `results`,
and its `flowInput` is what its step received.

## Input, output, and sandbox [#input-output-and-sandbox]

```ts
export const issueDesk = Agent({ id: "issue-desk", name: "Issue desk" })
  .input(z.object({ repo: z.string(), issue: z.number().int() }))
  .output(z.object({ url: z.string() }))
  .sandbox({ image: "node:24" })
  .step(triage);
```

* `.input(schema)` types `flowInput` and the first stage's `input`.
* `.output(schema)` types the flow agent's output, for a parent flow or a client.
* `.sandbox(spec)` declares the one sandbox its agents share. See
  [Sandbox](/docs/build/sandbox#in-a-flow-agent).

## Sessions and manifests [#sessions-and-manifests]

Use `saveAgent`, `createSession`, `input`, `observe`, `pending`, `approve`, and
`cancel` exactly as for agents. `session.input()` sends a string as `content`
and other JSON as `data`; the flow's first stage receives it.

On the wire a flow agent is a workflow manifest (`kind: "workflow"`,
`workflowSchemaVersion: 2`) that embeds every agent it runs, so one document and
one manifest hash cover the whole flow. `saveAgent(flowAgent)` saves that one
document; the agents inside it are not listed on their own.
`session.observe({ follow: true })` merges the linked agent sessions' events.
Studio draws the flow, its live stage status, loop iterations, and links to each
agent's session.

`Chain`, `Switch`, `Parallel`, `Map` and `Loop` still build the earlier
workflow manifest (`workflowSchemaVersion: 1`) and run as before. They don't
mix with flow agents in either direction.

## Examples [#examples]

See the shipped [`chain`, `switch`, `parallel`, `map`, `loop`, and
`ship-feature`](/docs/build/examples#flow-agent-examples) examples.

## Next step [#next-step]

Start with [Step](/docs/build/step), or open [Sessions](/docs/run/sessions)
to run a flow agent and [Studio](/docs/studio) to watch its progress.
