Graine AI

Flow API

Create, read and change flow agents with the /v2 agent endpoints — the same endpoints and API key as every other agent. The flow format field by field, and every check a save runs.

A flow agent is an agent with "agent_kind": "flow" and a flow. It is created, read and changed through the same /v2/agents endpoints as every other agent, with the same API key. A flow sent here opens in the flow builder, and a flow built in the builder reads back here.

Before you start

ConcernRule
Base URLhttps://api.graine.ai/v2
AuthenticationAuthorization: Bearer gat_.... See Authentication.
Errors{"error": <code>, "message": "<sentence>"}, as on every /v2 endpoint: branch on error. See Errors.
ApprovalsIn a workspace that reviews agent changes, a key a Member made can get 202 instead of a change: see Approvals.
Rate limitsThe /v2 limits apply. See Rate limits.
export GRAINE_KEY="gat_7f3c9a21b4e85d0c6a19f2734b8e5c60"
export GRAINE_API="https://api.graine.ai/v2"

Endpoints

Method and pathFor a flow agent
POST /v2/agentsCreate one: send "agent_kind": "flow" and a flow.
GET /v2/agents/{agent_id}Read it: agent_kind is "flow", and flow is its flow.
PATCH /v2/agents/{agent_id}Change it: flow is replaced whole; everything else merges.
GET /v2/agentsList agents. Each row says its agent_kind; flow is null on a list row, so read the agent for it.
POST /v2/callsCall with it as with any agent. The call's variables fill the flow's {names} and the fields its rules read.
POST /v2/calls/{execution_id}/eventsSend an event into the live call: see Events.

Checking a flow without saving it, and going back to a saved version, are in the flow builder: Check (see Test and fix a flow) and Saved versions (see Save and publish). Through the API, every save runs the same checks.

The flow format

A flow agent is an agent with "agent_kind": "flow" and a flow object. The flow describes the steps; the rest of the agent (its voice, model, languages, tools and call settings) is set the way it is for any agent.

Flow

FieldType
schema_versionintegerAlways 1 when you read a flow. Optional when you send one.
start_stepstep IDThe first step. Required.
briefingtextWhat every step knows: who the agent is, who it is calling and why. Each AI reply reads it before the step's instructions.
decisionobjectThe decision model, below. Optional.
field_typesobjectGives a rule field a type, string, number or boolean, keyed by the field exactly as rules write it: {"recipient_data.amount_due": "number"}.
defaultsobjectWhat a call falls back on when it doesn't send a value. A variable's default goes under recipient_data: {"recipient_data": {"business_name": "Acme Finance"}}. A call's own values always win.
stepsobjectEach step, keyed by its ID. At least one.

A step ID uses letters, digits and _, doesn't start with a digit, and is up to 48 characters. A - is also accepted, for IDs saved before this rule; the builder doesn't make new ones with it.

Decision

FieldType
providertextazure (the default) or sarvam. It can differ from the agent's own provider.
modeltextA decision model on that provider. Defaults to azure/gpt-4.1-mini on Azure and sarvam/sarvam-105b on Sarvam.
efforttextFor a GPT-5 decision model: none, low, medium or high. Left out, it thinks as little as the model allows. Not on Sarvam.
max_tokensintegerThe decision model's maximum answer, above 0. 250 when unset.
guidancetextRules for choosing paths, used on every step in place of the built-in instruction.

Models offered to flow agents, for replies and for decisions:

ProviderModels
azureazure/gpt-4.1-mini, azure/gpt-4.1-nano, azure/gpt-4o-mini, azure/gpt-4o-mini-2, azure/gpt-5.4-mini, azure/gpt-5.4-nano
sarvamsarvam/sarvam-105b, sarvam/sarvam-105b-conversations

Reasoning effort is none, low, medium or high on the two GPT-5 models and isn't accepted on the other Azure models. A Sarvam reply model takes low, medium or high, or no effort to keep thinking off; deciding on Sarvam takes none. Verbosity, a GPT-5 setting, is low, medium or high.

Step

FieldType
kindtextconverse (an AI reply), say (a fixed line) or route (a branch). converse when unset.
titletextThe step's name in the builder.
instructionstextconverse: what the AI does on this step. A route step has none.
saytext, or language → textsay: the line. One text, or one line per language: {"en": "…", "hi": "…"}.
sample_replieslanguage → textconverse: example replies, per language.
modelobjectconverse: this step's own reply model, on the agent's provider: model, provider, temperature, max_tokens and effort. Each one left out is the agent's. A step whose model is GPT-5 is saved at temperature 1.
decision_modeltextThis step's own decision model, on decision.provider.
decision_efforttextIts effort, for a GPT-5 decision model.
barge_in_wordsinteger or nullWords the caller can say over this step before it stops talking, 0 to 10. 0: it can't be interrupted. null: the agent's setting.
nudge_after_silence_snumber or nullRun the step again after this many seconds of silence, above 0. null: off.
force_tooltext or nullconverse: the tool the AI must call on this step: end_call, or the function_name of one of the agent's tools (call transfer's is transfer_phone). say: end_call hangs up once the line has played, on a step with no next paths.
knowledgeobjectconverse: this step's own knowledge base: collections, a list of knowledge bases' retrieval_ids from GET /v2/knowledge-bases, and similarity_top_k, how many passages to read (1 or more).
nextlistThe step's paths.

The language keys of say and sample_replies are the agent's languages: the keys of multilingual.languages on an agent that speaks several, else the short code of its speech recognition language, such as en or hi.

Path

FieldType
tostep IDWhere the path leads. Required.
labeltextWhat the arrow says in the builder.
whenobjectWhen to take the path, below. Required.
orderintegerRule paths are checked lowest first, and one without an order counts as 0. Among several always paths, the lowest is the fallback; among event paths for the same event, the lowest is taken.
collectname → type, or name → definitionFields the decision model fills in when it takes this path. A field is its type, string, number, integer or boolean, or a definition: type (default string), description, what the field means, and choices, the answers a string field may take (at least one). The decision model sees the description and the choices on the field. Each field becomes part of the call's data under its name, and its latest value reaches your webhook in collected_fields after the call. A field can't be named reasoning, confidence, recipient_data, detected_language, or start with _.
tool_nametextThe name the decision model knows this path by: 1 to 64 letters, digits, _ or -.
tool_descriptiontextWhat the decision model reads about this path, in place of when.description.

When

typeOther fields
aidescription: textThe decision model takes the path when the description fits.
rulematch: all (the default) or any; rules: a listTaken when all, or any, of the rules hold.
alwaysnoneThe fallback when nothing else matches. A route step must have one.
eventevent: the event's nameTaken when that event arrives while the call is on this step.

Each rule has a field, an op and, for most ops, a value.

opvalue
is, is_notOne value: text, a number, or true / false.
above, at_least, below, at_mostOne number.
one_of, none_ofA list of values, at least one.
containsOne value: text that contains it, or a list that holds it.
is_set, is_not_setNone. Whether the call has the field at all.

What a rule's field can read, and how values are compared, is on Using your data.

Layout

Where the builder draws each step is kept by the builder and isn't part of the API. A flow created here is laid out automatically the first time it opens in the builder, and keeps the positions you give it there.

The rest of the agent

Field
agent_kind"flow".
display_nameThe agent's name. Required when creating.
greeting_messageThe welcome message, said when the call connects.
configuration.runtime.language_modelThe reply model: provider, model and config (temperature, max_tokens, reasoning_effort, verbosity), and the agent's knowledge base as knowledge_base_config (enabled, collections, similarity_top_k). Leave agent_type out: a flow agent's kind is agent_kind, and a change that sets agent_type on a flow agent is refused with 409.
configuration.runtime.speech_recognitionHow the caller is heard.
configuration.runtime.voice_synthesisThe voice.
configuration.external_functionsThe agent's tools. A step's force_tool can name one by its function_name.
multilingualThe languages the agent speaks, when more than one.

The settings in the builder's Settings › On the call are stored where they are for every agent:

In the builderField
Ignore the caller until the welcome message is saidconfiguration.stream_policy.ignore_speech_before_greeting
Pause before the welcome messageconfiguration.stream_policy.greeting_delay_ms (0 to 1000)
Words before the caller can interruptconfiguration.stream_policy.bargein_word_threshold
Check the caller is still thereconfiguration.engagement_policy.presence_probe_enabled and presence_probe_after_seconds, and conversation_config.presence_probe_message
End the call after a long silenceconfiguration.session_policy.silence_close_after (seconds; 0 is off)
Maximum call lengthconfiguration.session_policy.session_ttl (seconds; 0 is no limit)
The agent hangs up when the conversation is overconfiguration.session_policy.close_after_reply, and conversation_config.completion_probe_prompt
Goodbye messageconversation_config.finale_message
Let the caller talk over a closing stepconfiguration.session_policy.finale_interruptible (false by default)
Detect voicemailconfiguration.session_policy.completion_probe, and conversation_config.probe_start_delay_seconds

Create a flow agent

POST /v2/agents. The body is an agent with "agent_kind": "flow" and its flow. This one is the payment reminder from the overview, with a fixed line for accounts that owe nothing and a way out for a caller who goes quiet.

payment-reminders.json
{
  "display_name": "Payment reminders",
  "agent_kind": "flow",
  "greeting_message": "Hello, this is Asha calling from {business_name}.",
  "configuration": {
    "runtime": {
      "language_model": {
        "provider": "azure",
        "model": "azure/gpt-4.1-mini",
        "config": { "temperature": 0.2, "max_tokens": 250 }
      },
      "speech_recognition": {
        "provider": "deepgram",
        "model": "nova-3",
        "language": "en",
        "endpointing": 190
      },
      "voice_synthesis": {
        "provider": "cartesia",
        "voice_id": "95d51f79-c397-46f9-b49a-23763d3eaa2d",
        "voice_name": "Arushi",
        "model": "sonic-3.5",
        "speed": 1.0
      }
    }
  },
  "flow": {
    "schema_version": 1,
    "start_step": "check_identity",
    "briefing": "You are Asha from {business_name}, calling {customer_name} about an overdue payment of {amount_due} that was due on {due_date}. Only discuss the account with {customer_name}. Be calm and respectful, and never threaten or pressure.",
    "decision": {
      "provider": "azure",
      "model": "azure/gpt-4.1-mini",
      "max_tokens": 250,
      "guidance": "Only choose a path once the caller has clearly answered. If they ask a question first, stay so the step can answer it."
    },
    "field_types": { "recipient_data.amount_due": "number" },
    "defaults": { "recipient_data": { "business_name": "Acme Finance" } },
    "steps": {
      "check_identity": {
        "kind": "converse",
        "title": "Check who it is",
        "instructions": "Ask whether you are speaking with {customer_name}. Don't mention the payment or the account until they confirm.",
        "sample_replies": { "en": "Hi, am I speaking with {customer_name}?" },
        "next": [
          {
            "to": "check_amount",
            "label": "Confirmed",
            "when": { "type": "ai", "description": "The caller confirms they are {customer_name}." }
          },
          {
            "to": "goodbye",
            "label": "Wrong person",
            "when": { "type": "ai", "description": "It is the wrong person, or they won't say who they are." }
          }
        ]
      },
      "check_amount": {
        "kind": "route",
        "title": "Check the amount due",
        "next": [
          {
            "to": "agree_date",
            "label": "Amount due",
            "order": 0,
            "when": {
              "type": "rule",
              "match": "all",
              "rules": [{ "field": "recipient_data.amount_due", "op": "above", "value": 0 }]
            }
          },
          { "to": "nothing_due", "label": "Nothing due", "order": 1, "when": { "type": "always" } }
        ]
      },
      "agree_date": {
        "kind": "converse",
        "title": "Agree a payment date",
        "instructions": "Tell them {amount_due} has been due since {due_date}. Ask when they can pay, and try to agree a date within the next seven days. Repeat the date back to confirm it.",
        "nudge_after_silence_s": 6,
        "next": [
          {
            "to": "goodbye",
            "label": "Date agreed",
            "when": { "type": "ai", "description": "The caller has given a date to pay, or says they can't commit to one." },
            "collect": { "promise_date": "string" }
          },
          {
            "to": "goodbye",
            "label": "No answer",
            "when": {
              "type": "rule",
              "match": "all",
              "rules": [{ "field": "_silence_repeats", "op": "at_least", "value": 2 }]
            }
          }
        ]
      },
      "nothing_due": {
        "kind": "say",
        "title": "Nothing due",
        "say": { "en": "Thank you. Our records show nothing is due on your account, so there is nothing you need to do." },
        "next": [{ "to": "goodbye", "label": "Next", "when": { "type": "always" } }]
      },
      "goodbye": {
        "kind": "converse",
        "title": "Goodbye",
        "instructions": "Thank the caller warmly, say goodbye, and end the call.",
        "sample_replies": { "en": "Thank you for your time. Have a good day!" },
        "force_tool": "end_call",
        "next": []
      }
    }
  }
}
curl -X POST "$GRAINE_API/agents" \
  -H "Authorization: Bearer $GRAINE_KEY" \
  -H "Content-Type: application/json" \
  -d @payment-reminders.json

The answer is 201 Created with the new agent, in the same shape a read returns. A new agent is a draft and starts at version 2, as with any agent. Its flow is what you sent, with schema_version, briefing and the decision provider and model filled in where you left them out. Abridged:

201 Created (abridged)
{
  "agent_id": "a3c1e0f2-6b7d-4f19-8e25-9d0c4b7a1e63",
  "display_name": "Payment reminders",
  "status": "DRAFT",
  "agent_kind": "flow",
  "version": 2,
  "greeting_message": "Hello, this is Asha calling from {business_name}."
}

Keep agent_id: it is the agent's id everywhere else in the API. Creating takes longer than a read, so give the request a client timeout of about two minutes.

StatusWhen
201Created.
400The body can't be an agent, for example an empty body.
403Your key can't create agents, or the body uses a cloned voice from another member's private Voice Library.
422Code 1001: a field is refused, agent_kind isn't "prompt" or "flow", or the flow has a problem that stops a save (below). Nothing is created.
429Rate limited. Retry after Retry-After seconds.
503, 504A service the request depends on is unavailable or slow. Retry with backoff.

Read a flow agent

GET /v2/agents/{agent_id} answers with the agent. For a flow agent it has "agent_kind": "flow" and the flow. A prompt agent has "agent_kind": "prompt" and "flow": null.

A flow comes back complete: every step has next, every AI reply has instructions, and schema_version, briefing and the decision provider and model are always present. A flow saved as

Sent
{
  "start_step": "greet",
  "steps": {
    "greet": {
      "kind": "converse",
      "instructions": "Greet the caller, find out what they need, and help them with it."
    }
  }
}

reads back as

Read back
{
  "schema_version": 1,
  "start_step": "greet",
  "briefing": "",
  "decision": { "provider": "azure", "model": "azure/gpt-4.1-mini" },
  "steps": {
    "greet": {
      "kind": "converse",
      "instructions": "Greet the caller, find out what they need, and help them with it.",
      "next": []
    }
  }
}

Change a flow agent

PATCH /v2/agents/{agent_id} with the fields to change.

  • flow is replaced whole. A step or path you leave out is deleted, so send the complete flow. The simplest way is to read it, change it and send it back.
  • Everything else merges into the agent as it does for any agent: send only what changes.
curl -s "$GRAINE_API/agents/$AGENT_ID" -H "Authorization: Bearer $GRAINE_KEY" \
  | jq '{flow: .flow} | .flow.steps.agree_date.instructions = "Ask when they can pay, and agree a date within the next 14 days."' \
  > change.json
 
curl -X PATCH "$GRAINE_API/agents/$AGENT_ID" \
  -H "Authorization: Bearer $GRAINE_KEY" \
  -H "Content-Type: application/json" \
  -d @change.json

The answer is 200 with the agent as stored. With a problem that stops a save, it is 422 and nothing is saved.

A live agent. An agent that is live (status ACTIVE) can't be changed: the answer is 409 with code 1201. Switch it off with {"status": "INACTIVE"} on its own, change it, and set {"status": "ACTIVE"} again. With a key a Member made, in a workspace that reviews changes, a change to a live agent becomes a request for an Admin's approval instead: see Approvals.

Flows saved before a check existed. If a save leaves the flow and the reply model as they are stored (a rename, a new voice), problems in the stored flow don't stop it.

Changing the kind. Send "agent_kind": "flow" with a flow to turn a prompt agent into a flow agent; its prompt is emptied, and its model must be one offered for flows. Send "agent_kind": "prompt" with prompts.conversation.system to turn a flow agent back into a prompt agent; its flow is dropped. Without agent_kind, an agent keeps its kind, and a flow sent to a prompt agent is refused.

A flow that uses most features

The flow below books product demos, in English and Hindi. Send it as the flow of an agent whose languages are English and Hindi; it passes every check with no warnings. Read it top to bottom:

  • Offer a demo answers questions from a knowledge base of its own, repeats itself after 7 seconds of silence, and gives up after two silent repeats. The path to Who should run the demo? collects demo_format, which can only be Live or Recording.
  • Who should run the demo? is a branch that never speaks: a rule on the collected demo_format, then a rule on team_size (typed as a number in field_types), then its always fallback.
  • Book with sales chooses its paths with its own decision model and effort.
  • Find a time replies with shorter answers than the agent's default, can't be cut in on by fewer than three words, and also moves on when your server sends the demo_booked event.
  • Send a recording and No thanks are fixed lines in both languages that hang up after their line; Confirm is an AI reply that ends the call.
  • The decision guidance uses a variable, which defaults fills when a call doesn't send it.
A demo-booking flow
{
  "schema_version": 1,
  "start_step": "offer_demo",
  "briefing": "You are Maya, calling for {company_name} to offer {first_name} a 20-minute demo of the product. Keep replies to one or two sentences. If they say no, accept it straight away.",
  "decision": {
    "provider": "azure",
    "model": "azure/gpt-4.1-mini",
    "max_tokens": 250,
    "guidance": "Choose a path only once {first_name} has clearly answered. If they ask a question first, keep the call on the step so it can answer."
  },
  "field_types": { "recipient_data.team_size": "number" },
  "defaults": { "recipient_data": { "company_name": "Northwind", "first_name": "there" } },
  "steps": {
    "offer_demo": {
      "kind": "converse",
      "title": "Offer a demo",
      "instructions": "Ask whether {first_name} would like a 20-minute demo this week, live with a person or as a recording. If they ask what it covers, answer in one or two sentences, then ask again.",
      "sample_replies": { "en": "Would a short demo this week be useful to you?" },
      "knowledge": { "collections": ["kb_product_notes"], "similarity_top_k": 5 },
      "nudge_after_silence_s": 7,
      "next": [
        {
          "to": "route_demo",
          "label": "Wants a demo",
          "when": { "type": "ai", "description": "The caller would like a demo." },
          "collect": {
            "demo_format": {
              "type": "string",
              "description": "Live if they want to go through it with a person, Recording if they would rather watch it in their own time.",
              "choices": ["Live", "Recording"]
            }
          }
        },
        {
          "to": "no_thanks",
          "label": "Not interested",
          "when": { "type": "ai", "description": "The caller isn't interested, or can't talk now." }
        },
        {
          "to": "no_thanks",
          "label": "Silent",
          "when": { "type": "rule", "match": "all", "rules": [{ "field": "_silence_repeats", "op": "at_least", "value": 2 }] }
        }
      ]
    },
    "route_demo": {
      "kind": "route",
      "title": "Who should run the demo?",
      "next": [
        {
          "to": "send_recording",
          "label": "Recording",
          "order": 0,
          "when": { "type": "rule", "match": "all", "rules": [{ "field": "demo_format", "op": "is", "value": "Recording" }] }
        },
        {
          "to": "book_with_sales",
          "label": "Large team",
          "order": 1,
          "when": { "type": "rule", "match": "all", "rules": [{ "field": "recipient_data.team_size", "op": "at_least", "value": 50 }] }
        },
        { "to": "find_time", "label": "Everyone else", "order": 2, "when": { "type": "always" } }
      ]
    },
    "book_with_sales": {
      "kind": "converse",
      "title": "Book with sales",
      "instructions": "Explain that for a team their size a specialist runs the demo. Ask which day and time this week suits them, and repeat it back to confirm.",
      "decision_model": "azure/gpt-5.4-mini",
      "decision_effort": "low",
      "next": [
        {
          "to": "confirm",
          "label": "Time agreed",
          "when": { "type": "ai", "description": "The caller has confirmed a day and time." },
          "collect": { "demo_time": { "type": "string", "description": "The day and time they confirmed, as they said it." } }
        }
      ]
    },
    "find_time": {
      "kind": "converse",
      "title": "Find a time",
      "instructions": "Ask which day and time this week suits them for the demo, and repeat it back to confirm. If they say they have booked on the page we sent, thank them.",
      "model": { "max_tokens": 120 },
      "barge_in_words": 3,
      "next": [
        {
          "to": "confirm",
          "label": "Time agreed",
          "when": { "type": "ai", "description": "The caller has confirmed a day and time." },
          "collect": { "demo_time": { "type": "string", "description": "The day and time they confirmed, as they said it." } }
        },
        { "to": "confirm", "label": "Booked online", "when": { "type": "event", "event": "demo_booked" } }
      ]
    },
    "confirm": {
      "kind": "converse",
      "title": "Confirm",
      "instructions": "Confirm the demo for {demo_time} and say an invitation will follow. Thank them and say goodbye.",
      "force_tool": "end_call",
      "next": []
    },
    "send_recording": {
      "kind": "say",
      "title": "Send a recording",
      "say": {
        "en": "No problem. We'll send you a recording of the demo today. Thanks for your time, and goodbye.",
        "hi": "कोई बात नहीं। हम आज ही आपको डेमो की रिकॉर्डिंग भेज देंगे। समय देने के लिए धन्यवाद।"
      },
      "force_tool": "end_call",
      "next": []
    },
    "no_thanks": {
      "kind": "say",
      "title": "No thanks",
      "say": {
        "en": "No problem at all. Thanks for your time, and have a good day.",
        "hi": "कोई बात नहीं। समय देने के लिए धन्यवाद, आपका दिन शुभ हो।"
      },
      "barge_in_words": 0,
      "force_tool": "end_call",
      "next": []
    }
  }
}

kb_product_notes stands for the retrieval_id of one of your knowledge bases, from GET /v2/knowledge-bases. An event that moves the call to Confirm should carry demo_time in its data, so the goodbye can say it.

Every check

Every create and change that sends a flow runs these checks before anything is saved. A check that fails stops the save with 422 and code 1001, and the message counts the problems and quotes the first:

422 Unprocessable Entity
{
  "error": 1001,
  "message": "The flow has 1 problem: Step 'Greet' leads to 'sales', which doesn't exist."
}

Warnings don't stop a save. To see every problem and warning at once, open the agent in the flow builder and choose Check.

These stop a save:

What is checkedRule
The flowflow is an object with at least one step. Every field has the right type: text, a whole number, a list or an object.
The first stepstart_step is set and names a step.
Step IDsLetters, digits, _ (and -), not starting with a digit or -; up to 48 characters.
Step kindskind is converse, say or route. say is text or language → text; sample_replies is language → text.
BranchesA route step has no instructions and no line, has an always path, and doesn't wait for an event. route steps don't lead to each other in a loop.
Pathsnext is a list. Each path has a to that names a step, and a when whose type is ai, rule, always or event.
Rulesmatch is all or any. At least one rule; each has a field, a known op, and the value its op takes.
EventsAn event path names its event.
Collected fieldsA field is a type, or a definition with only type, description and choices. The type is string, number, integer or boolean. choices is a list of at least one answer, on a string field only. No field is named reasoning or confidence (the decision model returns those with every choice), recipient_data or detected_language, or starts with _.
Path namesA tool_name is 1 to 64 letters, digits, _ or -, and isn't end_call, one of the agent's tools, or another name the platform reserves. Two AI-decided paths on one step don't share a name: two that lead to the same step need their own tool_names.
ToolsA step's force_tool is end_call or one of the agent's tools.
The reply modelprovider is azure or sarvam, and the model is one offered for flows. Its reasoning effort is one the model takes. Verbosity is only on GPT-5 models, and is low, medium or high.
A step's reply modelOn the agent's provider, a model offered there, with a number for temperature, a whole number above 0 for max_tokens, and an effort the model takes.
The decision modeldecision.provider is azure or sarvam, with a model offered there. Effort only for GPT-5 models, never on Sarvam. max_tokens is a whole number above 0. The same holds for a step's decision_model and decision_effort.
Step settingsbarge_in_words is a whole number from 0 to 10. nudge_after_silence_s is a number above 0.
KnowledgeA knowledge base that is on names at least one library, and similarity_top_k is a whole number of 1 or more.
Field types and defaultsfield_types maps a field to string, number or boolean. defaults is an object.

These are warnings, and the save goes through:

WarningWhy it matters
An ai path with no descriptionThe decision model has nothing to choose it by.
A converse step with no instructionsThe AI improvises on that step.
A say step with nothing to sayThe step plays nothing.
A say step without a line for one of the agent's languagesCallers in that language hear the English line, or the first one written.
A force_tool on a route step; on a say step, any tool but end_call, or end_call while the step has next pathsOnly an AI reply runs a tool, so it is ignored there, and a say step with paths moves on after its line instead of hanging up. Make the step a converse step, or remove the say step's paths.
Two rule or always paths on one step with the same orderTheir order is a tie, settled by the order they are listed in.
A step that no path leads to from the first stepNo call can reach it.

The flow builder adds a few warnings of its own, never errors: a temperature outside 0 to 2, a GPT-5 step not at temperature 1, a rule that compares a number with text or with an empty value, a collected field name that is hard to use in text, a tool_description without a tool_name, a step with its own decision model but no AI-decided paths, a line for a language the agent doesn't speak, an order below 0, and a path listed after an always path.

Approvals

When a key that a Member made changes a live agent, or sets "status": "ACTIVE", in a workspace that reviews agent changes, the change is held for an Admin and the answer is 202 Accepted. The live agent is unchanged until an Admin approves, and then the change is applied exactly as sent. Keys an Admin made, and keys made before roles existed, never get 202.

202 Accepted (abridged)
{
  "status": "pending_approval",
  "code": "approval_required",
  "detail": "Sent for review: “Payment reminders” is live, so these changes wait for a workspace admin's approval. The live agent is unchanged (request #12).",
  "approval_request_id": "apr_5c1f0e9a7b2d43e8a6f1"
}

The full answer also carries approval_request: the request itself, with its number, state and a link to it. Sending the same change again before it is decided updates the same request.

When "status": "ACTIVE" is sent with other fields to an agent that isn't live yet, the other fields are saved straight away; only going live waits.

On this page