Flow builder
A flow describes a conversation as connected steps: say something, wait for the user, route on what they said, call a service, answer. You edit it on a canvas in the dashboard, or as a JSON file in your repo. The same file runs in the agent, so you do not write the glue code yourself. The same flow answers in a voice call and in a text chat or data-only session. In a chat, replies are text only: nothing is spoken and no text-to-speech is charged (on current runners).
Open the Flow tab
Projects created from the flow-agent template have a Flow tab. Add a node from the Add a node row, drag the connections between nodes, and fill in the fields in the panel on the right. The next section lists every node type and field.
Node reference
Every node has an id (a letter first, then letters, digits, _ or -, up to 64 characters), a type and an optional label. Fields marked required must be present. Text fields hold up to 4000 characters, and a flow holds up to 500 nodes. Fields that name the next node (next, onErrorand so on) take another node's id.
speak
Say a line.
text(required): The line. Variables go inside the text as{{name}}.interruptible(optional, default true): Whether the user can talk over it.next(required): Node to run next.
listen
Wait for the user's next message.
timeoutMs(optional, default 8000): How long to wait, in milliseconds (1 or more).next(required): Node to run when something arrives.onNoInput(required): Node to run when nothing arrives in time.
route
Pick a path from what the user said. See the next section.
paths(required): Up to 20 paths. Each has an id, examples (required, up to 50 of at most 200 characters each), optional keywords, and next.input(optional): Text to classify. Defaults to the last thing the user said.onNone(required): Node to run when no path fits.onError(required): Node to run when the classifier call fails.timeoutMs(optional, default 3000): Classifier timeout in milliseconds.model(optional): Name of an entry under models.historyTurns(optional, default 0): How many earlier turns the classifier sees (0 or more).
condition
Branch on values.
rules(required): A rule or a group. A rule is var, op (eq, ne, gt, gte, lt, lte, contains, exists) and a value; exists needs no value. A group is all or any of rules or nested groups.then(required): Node to run when the rules hold.else(required): Node to run when they do not.
set
Store values in variables.
assign(required): Object of variable name to text, number or boolean.next(required): Node to run next.
http
Call an HTTPS endpoint.
method(required): GET, POST, PUT, PATCH or DELETE.url(required): Must start with https://.headers(optional): Object of header name to text, or to{ secret, prefix }to read the value from a project secret.body(optional): Request body.map(optional): Object that copies parts of the response into variables.timeoutMs(optional, default 5000): Timeout in milliseconds, 1 to 15000.next(required): Node to run on success.onError(required): Node to run on failure.
mcp
Call an MCP tool.
source(required): Name of a tool source.tool(required): Tool name.args(optional): Arguments for the tool.map(optional): Object that copies parts of the result into variables.timeoutMs(optional, default 15000): Timeout in milliseconds.next(required): Node to run on success.onError(required): Node to run on failure.
llm
Let a model write the answer.
instructions(required): What the model should do.send(required): conversation or message.message(optional): Text to send when send is message.historyTurns(optional): How many earlier turns to include (0 or more).model(optional): Name of an entry under models.speak(optional): Whether the reply is spoken (default false; set true to say it).saveAs(optional): Variable that receives the reply.tools(optional, default none): none, all, or a list of tool names.maxRounds(optional, default 5): Most tool rounds per run (1 or more).filler(optional):{ text, afterMs }: a line to say if the model is slow.next(required): Node to run next.onError(required): Node to run on failure.
wait
Pause.
ms(required): Milliseconds, 0 to 60000.next(required): Node to run next.
end
End the session.
reason(required): Why the session ends.text(optional): A last line to say.
The top level of the file has version (1), start (the first node), models and nodes, all required. Optional keys are secrets (names of project secrets the flow reads), variables (each with a type and an optional default) and tools (toolSources: the project's tool service or external MCP servers, used by mcp nodes and by llm nodes that call tools). Each entry in models names a provider (openai, anthropic, gemini or openrouter), a model, the project secret that holds the API key (apiKeySecret) and an optional baseUrl for an OpenAI-compatible endpoint, or points at another entry with use.
How routing decides
A routenode tries two cheap rules first. If the reply equals one path's example (ignoring case, extra spaces and a trailing ., ! or ?), that path wins. If no example matches, a keyword found as a whole word in the reply picks its path. When two paths match the same rule, or nothing matches, the classifier model decides: the node's model, else the classifier entry under models, else default. The model has to support tool calls. If it answers without one, or says no path fits, the flow follows onNone. If the call fails or takes longer than timeoutMs (3000 ms by default), the flow follows onError, and the error code is stored in error.<node id>. The chosen path id is stored in route.<node id>.
Route and llm nodes call the model with your own LLM key, stored as a project secret. See LLM agents.
Validation and saving
Every node is checked as you edit. A node with a problem shows an error count, and the list under the canvas names each error. Save stays off while any error is open. If someone else saved a newer revision first, the editor goes read-only and shows a conflict notice. Choose Reload to pick up their version, then redo your change. A flow saved by an older version of the package is upgraded when you open it, and the editor tells you to save to keep the upgrade. Saving writes voicethere.flow.json; publish the project to put it live.
Test the flow in the simulator
Test the flow runs the flow on mock data. Nothing leaves the dashboard: no HTTP request, MCP call or model call is made. Each http, mcp, llm and route node answers from the cases in voicethere.flow.mocks.json.
- Mock answers. Select a node and add cases: a Case with the answer to return, or an Error casewith an error code. The generator fills in a case from the node's shape; it is deterministic and takes a seed. In the test panel, each route node has a case picker; choose Pick each time to be asked which case to use when the run reaches it.
- Route mocks.A route case names the path to take. Use one of the node's path ids, or
noneto follow the node's no-match target (onNone); the editor lists the allowed paths under the case. A route error case takesROUTE_TIMEOUT,ROUTE_ERRORorLLM_CLASSIFIER_NO_CALL. - Step and Continue. With Step throughon, the run is paused before each node runs, so a node's mock (http, llm, mcp or route) is not used until you press Step or Continue. Step lets one node run. Continue runs until the next node with a breakpoint; pressing Step after Continue goes back to one node at a time. Right-click a node to set or clear a breakpoint.
Read a trace
Open trace loads a trace file written by flow run --trace out.json and highlights the nodes it visited. Replay a recent sessiondoes the same for a real session: pick one from the list or paste a session id. The replay reads at most the last 200 log lines of that session. If the session was longer, the editor says that earlier steps are missing. If the session ran an older version of the flow, steps for nodes you have since removed are greyed out. A session that took a route's no-match path, or hit a classifier error, replays along the same path.
Files in your repo
agent.ts imports the flow voicethere.flow.json the flow voicethere.flow.mocks.json mock cases for the simulator tools.ts custom tools for llm nodes
import { defineAgent } from "@voicethere/agent";
import { createFlowAgent } from "@voicethere/agent/flow";
import { openai } from "@voicethere/agent/llm/openai";
import flow from "./voicethere.flow.json";
import { tools } from "./tools";
defineAgent(createFlowAgent({ flow, tools, providers: [openai] }));The flow-agent template contains these files.
Run it locally
The agent package has a flow command that validates, upgrades and runs a flow on your machine.
npx @voicethere/agent flow validate [file]
npx @voicethere/agent flow migrate [file] [--write]
npx @voicethere/agent flow mock generate [nodeId] [--seed N] [--flow file] [--file mocks.json]
npx @voicethere/agent flow run [file] [--env file] [--mock [case]] [--record]
[--trace out.json] [--step] [--values]When file is left out, the command uses voicethere.flow.json in the current folder.
Build-time validation
A build with an invalid voicethere.flow.json fails before the agent is bundled. The error starts with invalid flow <path>: and has one line per problem, each with a code. For example, FLOW_URL_NOT_HTTPS means an http node or an MCP tool source has a URL that does not start with https://, and FLOW_URL_PRIVATE_HOSTmeans the URL's host is localhost or a private, loopback or link-local IP address. Host names are not resolved at this point.
CLI: export and import
Needs @voicethere/cli 0.20.0 or later.
voicethere flow export [--project <id>] [--out voicethere.flow.json] voicethere flow import <file> [--project <id>]
exportwrites the project's flow to a file, and also writes voicethere.flow.mocks.json when the project has one. import validates the file on your machine first, then uploads it. If voicethere.flow.mocks.json sits next to it, that file is uploaded too. See the CLI command reference for the other commands.