Conversation phases

Split a call into phases so each stretch of the conversation runs with its own prompt, tools, and knowledge

A simple agent answers every turn with one prompt and one set of tools. That is enough for a single-purpose bot, but a real call has stages — greet, verify identity, answer questions, wrap up — and each stage wants different behaviour. A flow lets you express that: a graph of conversation phases, each a stretch of turns with its own configuration, wired together by edges that decide where the call goes next.

The flow graph

A flow is a graph of nodes joined by edges.

  • A node is a step in the call. Every flow starts at a start node and finishes at an end node; in between sit the nodes that do the work — most often conversation phases, plus utility nodes like say, play_audio, tool_call, transfer_to_number, and agent_transfer.
  • An edge connects one node to the next. An edge fires on a condition: llm_condition (the model decides the named condition is met), expression (a variable-based rule), or default (taken when nothing else fires).

You read and write the graph on the agent:

  • GET /v1/agents/{agent_id}/flow — the current flow graph.
  • PUT /v1/agents/{agent_id}/flow — replace the agent’s draft graph. It is validated before it is stored; it does not go live until you publish.
  • POST /v1/agents/{agent_id}/flow/publish — publish the draft as a new active flow version.
  • GET /v1/agents/flow/schema — the JSON schema every node and edge validates against.

Drafts never touch live calls until you publish, and every publish is a version you can roll back (POST /v1/agents/{agent_id}/flow/rollback).

A flow graph is a JSON object with nodes and edges. Here is the smallest valid one — a single hosted phase between start and end. Save it as flow.json:

1{
2 "nodes": [
3 { "key": "start", "type": "start" },
4 {
5 "key": "main",
6 "type": "subagent",
7 "name": "Main",
8 "config": {
9 "instructions": "Greet the caller and help with their question.",
10 "exit_conditions": [
11 { "name": "caller_done", "description": "The caller has no more questions." }
12 ]
13 }
14 },
15 { "key": "end", "type": "end" }
16 ],
17 "edges": [
18 { "from": "start", "to": "main", "type": "default" },
19 { "from": "main", "to": "end", "type": "llm_condition", "condition": "caller_done" }
20 ]
21}

Set AGENT_ID and SPEECHIFY_API_KEY, then read, replace, and publish:

$# Read the current flow graph
$curl "https://api.speechify.ai/v1/agents/$AGENT_ID/flow" \
> -H "Authorization: Bearer $SPEECHIFY_API_KEY"
$
$# Replace the draft graph (validated, not yet live)
$curl -X PUT "https://api.speechify.ai/v1/agents/$AGENT_ID/flow" \
> -H "Authorization: Bearer $SPEECHIFY_API_KEY" \
> -H "Content-Type: application/json" \
> -d @flow.json
$
$# Publish the draft as a new active version
$curl -X POST "https://api.speechify.ai/v1/agents/$AGENT_ID/flow/publish" \
> -H "Authorization: Bearer $SPEECHIFY_API_KEY"

A conversation phase

A conversation phase (the subagent node type on the wire) is one stretch of turns run against a single brain, ending when an exit condition fires or a turn ceiling is hit. Its config carries everything that makes this stretch of the call different from the rest:

FieldWhat it does
brain_idWho answers this phase. Absent → Speechify’s hosted model under the overrides below. Set → an external brain: your own HTTPS service, which reaches no LLM.
instructionsPhase-specific instructions appended on top of the agent’s base prompt. Use for “you are now verifying the caller’s identity.”
system_prompt_overrideReplace the base prompt entirely for this phase. Mutually exclusive with instructions.
first_messageSpoken once on entry — good for explicit transitions (“Let me pull up your account.”).
tools_filterNarrow the tools available in this phase to a subset of the agent’s tools.
knowledge_base_filterNarrow which of the agent’s knowledge bases this phase may search. See Scope knowledge per phase.
variablesPer-phase variable overrides that win over the agent- and flow-level values while this phase renders.
exit_conditionsNamed conditions that end the phase, evaluated after every turn; each outgoing llm_condition edge matches one by name.
max_turnsTurn ceiling before the runtime force-takes the phase’s default edge. 0 means no cap for a hosted phase. An external brain with no cap does not run unbounded — it falls back to a built-in ceiling, so a failing third-party service can’t hold the call open indefinitely. Set an explicit max_turns either way.

Every override is scoped to the phase. Outside it, the agent falls back to its base prompt, its full tool set, and its full knowledge scope.

Hosted and external brains

Who answers a phase is a field, not a different kind of node.

  • Leave brain_id unset and the phase runs on Speechify’s hosted model, shaped by the phase’s prompt, tool, and knowledge overrides.
  • Set brain_id to an external brain — a workspace resource that names your own HTTPS endpoint — and the phase relays every turn to that endpoint. The prompt/tool/knowledge overrides do not apply — your service decides what to say. It ends its phase by replying with action: "handoff" naming an exit condition, the same exit surface a hosted phase reaches by tool call.

Because the brain is a reference, two agents can point at the same external brain, and rotating its signing secret never requires re-saving a flow.

Exit conditions and edges

A phase ends when one of its named exit_conditions is met. Each condition maps to an outgoing llm_condition edge, so “caller verified” routes to the answering phase while “verification failed” routes to a transfer. A phase with no condition met before max_turns takes its default edge (or ends the call if there is none).

A conversation phase keeps the wire type subagent because published flows already carry it. The console and these guides call it a conversation phase.

Good to know

Knowledge bases and tools attach to the agent. A phase’s knowledge_base_filter and tools_filter can only ever narrow that set for the phase — a phase can never reach a knowledge base or tool the agent is not attached to.

A phase’s variables override the agent- and flow-level values only while the phase renders its instructions and first message. They are not written back to the shared store.

Every POST /flow/publish mints a version with a parent. Roll back with /flow/rollback, list history with /flow/versions, and take a flow offline with /flow/deactivate.