Flow agents
Compose agents, tools, and your own functions into an agent whose code decides what runs next.
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.
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:
import { report } from "./report/agent.js";
export const agents = [report];Stages
Every stage has the same shape, .stage(whatRuns, { how }), and every stage
accepts id and input.
| Stage | Behavior |
|---|---|
.step(x, { id?, input? }) | Runs one agent, tool, flow agent or flow(); its output is the next input |
.switch({ ...cases, default? }, { on }) | on returns a case name; exactly that case runs |
.parallel(branches) | Runs named branches at once on the same input; output is keyed by branch |
.map(each) | Runs each once per item of its input, which must be a list |
.loop(body, { verify, max?, decide? }) | 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
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:
.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()
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:
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
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
idyou 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
A flow agent can be a step of another flow agent, or a subagent of a ReAct agent:
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
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)typesflowInputand the first stage'sinput..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.
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
See the shipped chain, switch, parallel, map, loop, and
ship-feature examples.
Next step
Start with Step, or open Sessions to run a flow agent and Studio to watch its progress.