Part 5 — Call flow reference
← Contents · Previous: Part 4 — Reference · Next: Part 6 — Building the screens
Everything inside a flow document: its parts, messages, variables, the 18 blocks with every setting and exit, the Waiting Room, the AI Agent, and every check code. It's written for the developer of a flow editor. What the editor does with the API (push, approve, put on a number) is in Part 2B. How to build the editor screen is in Part 6 §6.1.
Two endpoints keep an editor honest:
GET /v1/scripts/schema(2A, A2): block types, groups, exits and which need approval, generated from the engine's code. Build the block library from it.POST /v1/scripts/validate(2A, A3): the same checks as a push. Run it on every save and show the issues on the blocks (§5.9).
Conventions in this page:
- Default is what the engine uses when the field is absent. Leave a field out rather than sending the default.
- A setting marked req must be present, or the check reports an error.
- ⚠ next to a block = a risky block: a version with it should be approved before it runs (Part 1 §1.6).
5.1 The flow document
{
"direction": "OUTGOING",
"settings": { "language": "sw", "voice": { "sw": "marin", "en": "cedar" }, "speed": 1.0, "record": "off" },
"vars": [ { "name": "name", "type": "text" }, { "name": "amount", "type": "money" } ],
"start": { "out": "hello" },
"blocks": {
"hello": { "type": "say", "msg": { "src": "text", "text": "Habari {name}. Deni lako ni shilingi {amount}." },
"x": { "next": "menu" } },
"menu": { "type": "menu", "msg": { "src": "text", "text": "Bonyeza moja kulipa sasa, mbili kuongea na mhasibu." },
"options": [ { "k": "1", "id": "pay", "label": "Lipa", "words": ["lipa", "kulipa"] },
{ "k": "2", "id": "staff", "label": "Mhasibu", "words": ["mhasibu", "mtu"] } ],
"speech": { "match": "words" },
"x": { "pay": "pay", "staff": "desk", "noInput": "bye", "notUnderstood": "bye" } },
"pay": { "type": "app_action", "action": "pay", "params": { "amount": "{amount}" }, "countAs": "pay_started",
"x": { "paid": "thanks", "failed": "bye", "timeout": "bye" } },
"desk": { "type": "connect", "department": "c09c1657-…", "x": { "noAnswer": "bye" } },
"thanks": { "type": "goodbye", "msg": { "src": "text", "text": "Asante, malipo yamepokelewa." } },
"bye": { "type": "goodbye" }
}
}
(The Swahili reads: "Hello {name}. Your debt is {amount} shillings." · "Press one to pay now, two to talk to the accountant." · "Thank you, the payment was received.")
| Field | Type | Required | Description |
|---|---|---|---|
direction |
string | Yes | INCOMING (a number plays it), OUTGOING (call-outs, quick calls, scheduled calls play it) or SHARED (only other flows run it, through Transfer to script). Fixed for the flow: every version has the same one |
start |
object | Yes | The first block: {"call": id} for INCOMING, {"out": id} for OUTGOING, {"in": id} for SHARED |
blocks |
object | Yes | Block id → block. At most 60 blocks |
vars |
array | No | OUTGOING only: the values each person on a call-out brings ({name, type}; type is text (default) or money). Not allowed in INCOMING flows (VARS_IN_INCOMING) |
inputs |
string[] | No | SHARED only: what a Transfer to script block must pass in; used inside as {in.<name>} |
settings.language |
string | No | sw or en: the language the flow's fixed texts are made into audio in when the version is pushed. During a call, see §5.2 |
settings.voice |
string or object | No | The voice for every text: "marin", or one per language {"sw": "marin", "en": "cedar"} |
settings.speed |
number or object | No | 0.7–1.5 (rounded to 0.05), or one per language. Default 1.0 |
settings.record |
string | No | "off": no live recording for calls on this flow, even if the account records. Absent: the account decides |
Block ids
- Any string that is unique in the flow (
menu,pay_1,b7). The id is what exits point to, what the timeline names, and what per-block stats count by (2B, D9). - Keep an id when the block is edited, so stats stay comparable across versions. Make a new id only for a new block.
Every block's shape
"pay": {
"type": "app_action", // required: one of the 18 types
"…settings…": "…", // the block's own settings (§5.5)
"before": { "src": "text", "text": "Mazungumzo haya yanarekodiwa." }, // optional announcement (§5.4)
"countAs": "pay_started", // optional outcome tag (§5.4)
"voice": "cedar", "speed": 1.1, // optional: this block's voice and speed
"wr": { … }, // Waiting Room, on Connect / Send request / App action (§5.6)
"x": { "paid": "thanks", "failed": "bye", "timeout": "bye" } // exit name → block id
}
Exits
xmaps each exit name to the id of the next block.- An exit that is missing or empty ends the call with the system goodbye ("Asante kwa kupiga simu. Kwaheri." /
"Thank you for calling. Goodbye."). The check warns
EXIT_UNCONNECTEDfor each one, and the timeline recordsEXIT_UNCONNECTEDwhen a call ends there. A warning is not an error: sometimes ending there is what the author wants. - Loops are allowed. An exit may point back to an earlier block or to the block itself ("press 0 to repeat",
back to the menu). A loop is stopped only when it goes 25 blocks in a row without the caller pressing a key,
speaking or recording (
LOOP_GUARD), so a caller going round a menu is never cut off. - Exit names a block doesn't have get the warning
EXIT_UNKNOWN; they're ignored. - Not warned when unwired:
later(optional), and exits the block can't take as set up (notUnderstoodon a keys-only menu; Schedule'snotUnderstoodwithoutask,declinedwithout confirmation).
5.2 Messages
Every place a flow speaks (msg, before, ask, wr.start, wr.updates, thanks) takes a message: one
part, or a list of parts played one after another.
{ "src": "text", "text": "Habari {name}", "lang": "sw", "voice": "marin", "speed": 1.0 }
{ "src": "upload", "media": "7f1c…" }
[ { "src": "upload", "media": "7f1c…" }, { "src": "text", "text": "Deni lako ni {amount}." } ]
("Hello {name}" · "Your debt is {amount}.")
| Field | Description |
|---|---|
src |
text (the engine speaks it), or record / upload / library (an audio file, by media id). The three audio sources play the same way; the name only says where the file came from, for the editor |
text |
Up to 500 characters. May contain {variables} (§5.3) |
media |
The id of an audio file of this account (2B, E1). It must be READY when the version is pushed |
lang |
sw or en: the language of this text. Absent: the call's current language |
voice, speed |
This message's voice and speed. Absent: the block's, then the flow's settings, then the engine's default |
When audio is made. Fixed texts (no {) are turned into audio when the version is pushed, so calls play them at
once. Texts with variables are made during the call the first time each exact text is needed, then reused.
Changing the voice or speed makes new audio.
The call's language ({lang}) starts as the account's defaultLanguage. It changes when the caller picks a menu
option with lang, or when an AI Agent with AUTO language hears the caller's language. Engine messages ("Samahani…",
the goodbye, Pick's "Bonyeza…") follow it. For a flow written in a language other than the account's default, put
lang on each text, or start with a menu option that sets the language.
Voices: marin · coral · shimmer · sage · cedar · ash · echo · ballad · verse.
5.3 Variables
{name} in any text, URL, parameter or body is filled during the call. A variable with no value becomes empty, and
the timeline records VARIABLE_MISSING. The check refuses names it can't know (UNKNOWN_VARIABLE).
Always there
| Variable | Value |
|---|---|
{caller} |
The other person's number: the caller (incoming) or the person called (outgoing). E.164 |
{number} |
The business's number on this call |
{callId} |
The call's id |
{date} · {time} |
Now, in the account's time zone: 13/11/2026 · 14:05 |
{lang} |
The call's language: sw or en |
{key} |
The last menu key pressed |
{heard} |
The last thing the caller said (any Ask block) |
{answeredBy} |
Outgoing calls: person or machine (answering-machine detection). Detection runs only for flows that use this variable |
Made by blocks
| Variable | Made by | Example |
|---|---|---|
{<var>} |
the call-out person's values (OUTGOING vars) |
{name}, {amount} |
{input.<saveAs>} |
Collect number | {input.account} |
{answer.<saveAs>} |
Ask question | {answer.problem} |
{poll.<saveAs>} |
Poll (saveAs, else the block id) |
{poll.rating} = 4 |
{choice.label} · {choice.index} · {choice.<field>} |
Pick from list | {choice.id}, {choice.price} |
{reply.code} · {reply.say} · each map target |
Send request | {reply.code} = 200; "map": {"data.balance": "balance"} → {balance} |
{action.<block>.outcome} · {action.<block>.<key>} |
App action that waits (the data of its result) |
{action.pay.receipt} |
{ai.<field>} |
AI Agent fields | {ai.date} |
{schedule.when} · {schedule.at} |
Schedule call | "kesho saa tatu asubuhi" · 2026-11-14T09:00 |
{in.<input>} |
inside a SHARED flow: what the Transfer block passed | {in.account} |
{queue.position} |
Connect, while waiting | 3 |
How they're read aloud:
moneyvariables are read with thousands separators: 45000 → "45,000".- Numbers in
{input.*}and{poll.*}are read digit by digit, in words. - Sensitive values (Collect with
sensitive) are usable in the call but shown as••••in the timeline, webhooks and exports.
5.4 Settings any block can have
| Field | Description |
|---|---|
before |
Announcement: a message played once when the block starts, before its own prompt. Not repeated when the block asks again after a wrong key ("Mazungumzo haya yanarekodiwa", "This call is recorded") |
countAs |
Outcome tag: when a call reaches the block, the tag is added to the call (booked, promised, wants_callback). Reports and call-out stats count calls by tag. Some are added automatically: opted_out (Opt out), callback_scheduled (Schedule call), App action outcomes the client app mapped with countAs |
voice, speed |
The block's voice and speed, for its texts that don't set their own |
| Field | Default | Description |
|---|---|---|
timeoutSec |
8 (Collect 10) | Seconds of silence after the prompt before it counts as no input |
retries |
2 | How many times the block asks again after no input, a wrong key or words it didn't understand. 0–3 |
laterWords |
— | Extra words meaning "not now" for the later exit (below) |
- Keys always work. A Menu, Poll or Pick also listens for speech when it has a
speechsetting; Collect whenspeech: true; Ask question always listens. - The caller can press a key or start talking during the prompt: the prompt stops (barge-in).
- After all retries, the block leaves through its give-up exit (
noInput,notUnderstood,failedorsilence). laterexit (Menu, Collect, Poll, Pick): when it is wired, words like "baadaye", "niko busy", "sina muda", "later", "call me back" take it instead of a retry. Wire it to a Schedule call block. When it isn't wired, those words are treated like any other answer.
5.5 The blocks
| Group | Blocks |
|---|---|
| Speak | Say |
| Ask | Menu · Collect number ⚠ · Poll · Pick from list · Ask question |
| Connect | Connect to department · Voicemail · Schedule call · AI Agent ⚠ |
| Integrate | Send request ⚠ · App action (⚠ when the action is risky) |
| Logic | Condition · Go to · Transfer to script · Return |
| End | Opt out · Goodbye |
say
Says a text or plays audio, then goes on.
| Setting | Type | Default | Rules |
|---|---|---|---|
msg |
message | req |
Exits: next.
Timeline: SAY / PLAY.
"hi": { "type": "say", "msg": { "src": "text", "text": "Karibu PMK Chemicals." }, "x": { "next": "menu" } }
("Welcome to PMK Chemicals.")
menu
Asks the caller to pick an option by key or, with speech on, by voice. Each option is an exit.
| Setting | Type | Default | Rules |
|---|---|---|---|
msg |
message | req | Say the keys and words: "Bonyeza moja au sema mauzo…" ("Press one or say sales…") |
options |
array | req | 1–10 options (below) |
speech |
object | absent = keys only | {"match": "words" | "smart", "minConfidence": 0.5, "language": "sw", "maxSec": 12} |
timeoutSec · retries · laterWords |
8 · 2 | §5.4 |
Each option:
| Field | Description |
|---|---|
k |
The key: 0–9, * or #, unique in the menu. May be empty only when speech is on (a voice-only option) |
id |
The exit name. Default: the key. Letters, digits, _; unique in the menu |
label |
The option's name, read back when the caller isn't understood ("Samahani, sijaelewa. Unaweza kusema: Mauzo au Msaada.", "Sorry, I did not understand. You can say: Sales or Help.") |
words |
Words that pick this option by voice (["mauzo", "kununua", "bei"]). With match: words, an option without words can only be picked by its key (warning OPTION_WITHOUT_WORDS) |
lang |
sw or en: picking this option also switches the call's language (a language menu) |
speech.match:
words: the caller's words are compared with each option'swords(exact or close).smart: words first; if none match, the AI picks the option the caller meant from labels and words ("nataka kuongea na mtu wa malipo", "I want to talk to someone about payments"). BelowminConfidence→ not understood. Needs the client app'sAIfeature. Without it, the push is refused; if AI is taken away later, calls fall back towords.
Exits: each option's id · noInput (silence or wrong keys, after retries) · notUnderstood (speech that
matched nothing, after retries; if not wired, noInput is used) · later.
Flows from before 2026-11-12 used keys instead of options and the exit noKey. Both still run; write options and noInput.
Timeline: KEY (digit, valid) · SPEECH · MATCH (option, by: key / words / smart, confidence) · NO_INPUT · RETRY · LANGUAGE · LATER.
Webhook: call.block {type: "menu", option, by}.
collect
⚠ Risky. Collects a number (account, meter, PIN, amount) into {input.<saveAs>}.
| Setting | Type | Default | Rules |
|---|---|---|---|
msg |
message | req | "Weka namba ya akaunti, kisha alama ya reli." ("Enter your account number, then the hash key.") |
saveAs |
string | req | A variable name: [a-zA-Z_][a-zA-Z0-9_]* |
minDigits · maxDigits |
int | 1 · 20 | 1–20, min ≤ max |
finishKey |
string | # |
The key that ends the number |
pattern |
string | — | A regular expression the digits must match (^07\d{8}$) |
sensitive |
bool | false | PIN-like: kept encrypted, shown as •••• everywhere; never listens for speech |
speech |
bool | false | Also accept a spoken number ("sifuri saba moja…", "zero seven one…"). Off by default: spoken account numbers are where speech recognition errs most |
timeoutSec · retries · laterWords |
10 · 2 |
A wrong length or pattern → "Namba uliyoingiza si sahihi." ("The number you entered is not valid.") and ask again.
Exits: ok · failed (after retries) · later.
Timeline: INPUT (value or ••••, valid, by: key / voice) · NO_INPUT · NOT_UNDERSTOOD · RETRY.
Webhook: call.block {type: "collect", saveAs, value} (masked when sensitive).
poll
A question answered with a key from 1 to 5 (rating, yes/no, survey), saved as {poll.<saveAs>}.
| Setting | Type | Default | Rules |
|---|---|---|---|
msg |
message | req | Say what each key means |
answers |
int | 5 | 1–5: keys 1..answers are valid |
saveAs |
string | the block id | |
labels |
array | — | [{"k": "1", "label": "Mbaya", "words": ["mbaya"]}, …]: names for reports, and words for speech |
speech |
bool or object | off | true, or {"match": "words" | "smart", "minConfidence"}. A spoken number ("tano", "five") always works |
timeoutSec · retries · laterWords |
8 · 2 |
Exits: next (answered) · noInput · later.
Timeline: POLL (answer, label, by) · KEY · NO_INPUT · NOT_UNDERSTOOD · RETRY.
Webhook: call.block {type: "poll", question, answer, label}.
pick
Reads a list ("Bonyeza moja kwa Arusha. Bonyeza mbili kwa Dodoma…", "Press one for Arusha. Press two for Dodoma…") and
lets the caller choose. Sets {choice.*}.
| Setting | Type | Default | Rules |
|---|---|---|---|
source |
string | req | The id of a Send request block that waits for its reply (the reply's top-level items array), or "inline" |
items |
array | — | With source: "inline": [{"id": "aru", "label": "Arusha"}, …] |
label |
string | {label} |
How each item is read: fields of the item, "{name} kwa shilingi {price}" ("{name} for {price} shillings") |
msg |
message | — | Said once before the list |
perPage |
int | 5 | 1–9 items per page; 0 reads the next page |
speech |
bool or object | off | The caller can say the item's name, its number, or "zaidi" / "more" |
timeoutSec · retries · laterWords |
8 · 2 |
Exits: chosen · empty (no items) · noInput · later.
Variables: every plain field of the chosen item as {choice.<field>}, plus {choice.label} and {choice.index}.
Timeline: PICK (index, id, label, by) · PICK_MORE · PICK_EMPTY · KEY · NO_INPUT.
Webhook: call.block {type: "pick", id, label}.
ask
Asks an open question; the caller's spoken answer becomes text in {answer.<saveAs>} and the call goes on.
("Tuambie tatizo lako.", "Tell us your problem.") Unlike Voicemail, nothing is recorded and the call doesn't end.
| Setting | Type | Default | Rules |
|---|---|---|---|
msg |
message | req | |
saveAs |
string | req | Variable name |
maxSec |
int | 20 | 5–60: the longest answer |
language |
string | the call's | sw / en: the language to listen for |
useMenuWords |
bool | false | If the Menu just before was answered by voice in 6 or more words, those words are the answer and the question is skipped |
retries |
int | 2 | Asked again after silence |
Exits: answered · silence.
Timeline: SPEECH · NO_INPUT · RETRY · ANSWER_FROM_MENU.
Webhook: call.block {type: "ask", saveAs, text}.
connect
Puts the caller in a department's queue and rings its agents in their browser (Part 2D). The caller waits in the Waiting Room until an agent accepts.
| Setting | Type | Default | Rules |
|---|---|---|---|
department |
string | req | A department id of this account (not deleted) |
record |
bool | account | false: don't record the talk with the agent, even if the account records |
wr |
object | engine defaults | The Waiting Room (§5.6): music, updates, queue position, option keys, give-up time |
Agents are rung once the caller has heard everything before the music (the intro, wr.start). Whether members'
mobiles ring too is the number's ringMode (2A), not a block setting.
Exits: noAnswer (nobody accepted before wr.giveUpSec; or nobody online and the department says "exit at once")
· each wr.options[].exit.
After an agent accepts, the flow doesn't go on: the call is with the agent until someone hangs up.
Timeline: WAIT_START · QUEUED · RING · RING_TIMEOUT · RING_DECLINED · ACCEPTED · AGENT_JOINED · LIVE_START · NOBODY_ONLINE · WAIT_UPDATE · WAIT_OPTION · WAIT_END.
voicemail
A prompt, a beep, then records the caller's message. Ends the call. The message is stored, transcribed if the account
transcribes, and sent as voicemail.received.
| Setting | Type | Default | Rules |
|---|---|---|---|
msg |
message | req | "Acha ujumbe baada ya mlio." ("Leave a message after the beep.") |
maxSec |
int | 60 | 5–120, and never above the account's voicemail limit |
beep |
bool | true | One beep to start, two short beeps warnBeforeEndSec before the end, one long beep when time is up. Only the caller hears them; they're not in the recording |
warnBeforeEndSec |
int | 10 (a third of a short maxSec) |
0 = no warning |
thanks |
message | the system goodbye | Said after the message |
The caller ends the message with # or by hanging up.
Exits: none (the call ends).
Timeline: VOICEMAIL (recorded) · RECORDING_READY (kind: VOICEMAIL) · TRANSCRIPT_READY.
schedule
Books a later call to this caller ("nipigie baadaye", "nikumbushe kesho asubuhi", "call me later", "remind me tomorrow
morning"), played by an OUTGOING flow. Needs the client app's CALLOUTS feature. Full design: SCHEDULE-CALL.md.
| Setting | Type | Default | Rules |
|---|---|---|---|
script · version |
string · int | req | An OUTGOING flow of this account and a runnable version of it |
after |
string | — | A fixed delay: 30m, 1h, 1d |
at |
string | — | A fixed time: 17:00, today 17:00, tomorrow 09:00 (a time already past today means tomorrow) |
ask |
message | — | The caller says the time ("Tukupigie lini?", "When should we call you?") |
confirm |
bool | true with ask, else false |
Say the time back and wait for yes / no (keys 1 / 2 too) |
vars |
object | — | {name: template}: values for the later call's flow ({"order": "{input.order}"}) |
note |
string | — | What it's about, shown on the scheduled call |
maxTries · retryGapSec |
int | 2 · 300 | 1–3 tries; at least 300 s apart |
msg |
message | engine line | Said after booking; may use {schedule.when} |
timeoutSec · retries |
8 · 2 | For the spoken time and the yes / no |
Exactly one of after, at, ask.
Rules the engine applies, whatever the flow says:
- 5 minutes to 30 days ahead. A time too soon is moved up to the earliest allowed minute.
- Quiet hours move the time, and the read-back says so.
- A new booking for the same caller and flow replaces the old one; at most 3 waiting per caller.
- Times are understood by the engine's rules first ("kesho saa tatu asubuhi", "baada ya saa moja", "tomorrow at 9").
Other wordings ("tarehe 30") use the AI, only when the client app has
AI.
Exits: scheduled · notUnderstood (the time wasn't understood, after retries) · declined (the caller said no
to the time, after retries) · refused (the engine said no; SCHEDULE_REFUSED.reason: TIME_OUT_OF_RANGE, OPTED_OUT, TOO_MANY_PENDING,
SCRIPT_NOT_RUNNABLE, FEATURE_OFF, NO_TIME).
Timeline: SCHEDULE_HEARD (said, at) · SCHEDULE_CHANGED · CALL_SCHEDULED (at, script) · SCHEDULE_REFUSED (reason).
Webhook: call.scheduled {callId, calloutId, at, to, scriptRef, said}.
ai_agent
⚠ Risky. A spoken conversation with an AI, in stages, with its own knowledge, fields and tools. Needs the client app's AI
feature. Its seven exits lead to any block. Full settings: §5.7.
Exits: goal (done) · human (wants a person: usually a Connect) · vm (leave a message) · cb (call me back)
· nohelp (the caller ended it) · time (maxMin reached) · err (AI failed, didn't start in 20 s, or the
client app lost AI).
request
⚠ Risky. Calls the business's own HTTPS endpoint and maps the JSON reply into variables.
| Setting | Type | Default | Rules |
|---|---|---|---|
url |
string | req | https:// with a public host. {variables} in it are URL-encoded. Private, loopback and cloud-metadata addresses are refused |
method |
string | POST |
GET · POST · PUT · PATCH · DELETE |
params |
object | — | Query parameters {name: template} |
headers |
object | — | {name: template}. X-Voice-*, Host and Content-* are set by the engine |
body |
object | {"type": "json"} |
type: json · form · raw · none; value: the body, with variables |
auth |
object | {"type": "signature"} |
signature (signed with the account's request secret) · bearer + token · basic + username + password · apikey + header + value · none |
map |
object | — | Reply JSON path → variable: {"data.balance": "balance", "items[0].name": "first"} |
timeoutSec |
int | 10 | 1–15 |
mode |
string | reply |
reply: wait for the answer · continue: send and go on at once (queued, retried for up to 24 h) · callback: the business answers later at {callback_url} |
wr |
object | — | The Waiting Room for reply / callback when the answer takes more than about a second |
What the engine sends and expects:
- Signed requests carry
X-Voice-Timestamp,X-Voice-Signature(hex HMAC-SHA256 oftimestamp.body),X-Voice-Idempotency-KeyandX-Voice-Call-Id. JSON bodies get ametaobject (callId,callOutId,block). - 2xx =
success. Any other status, a redirect (never followed), a reply over 64 KB, a blocked address or a network error =failure. No reply in time =timeout. - A top-level
sayin the reply becomes{reply.say}. A top-levelitemsarray feeds Pick from list. callbackmode: the body must contain{callback_url}(checkCALLBACK_URL). The business POSTs the result there beforewr.giveUpSec."result"or"status"set tofailure,failed,error,declinedorcancelled→failure; anything else →success.- Secrets (
auth.token,auth.password,auth.value) are encrypted when the version is pushed and come back as"(hidden)"from D6. Push"(hidden)"back unchanged to keep the stored secret of the same block id in the latest version; send a new value to replace it. A"(hidden)"on a block that had no secret is refused (REQUEST_AUTH).
Exits: continue mode: next. Other modes: success · failure · timeout · each wr.options[].exit.
Timeline: REQUEST_SENT · REQUEST (status, latencyMs, outcome, saved) · REQUEST_QUEUED · REQUEST_CALLBACK · REPLY_NOT_JSON · WAIT_*.
Webhook: request.failed (a continue request that failed for 24 h); call.block with late: true when a callback arrives after the caller left.
app_action
Asks the client app to do one of the actions it registered (Part 2E): send an SMS, take a payment, create a ticket. The engine has no built-in SMS or payment.
| Setting | Type | Default | Rules |
|---|---|---|---|
action |
string | req | A name the client app registered |
params |
object | the action's defaults | {name: template}. Every required parameter must be given; values are type-checked during the call (money, number, phone, choice) |
msg |
message | — | Said before waiting ("Tafadhali thibitisha malipo kwenye simu yako.", "Please confirm the payment on your phone.") |
wr |
object | — | The Waiting Room while it waits |
Exits:
- Action that doesn't wait:
next. A bad parameter or unknown action →failed. - Action that waits: each registered outcome (
paid,failed_pin…) ·failed·timeout(the action'stimeoutSec, default 120) · eachwr.options[].exit.
Variables: {action.<block>.outcome} and each key of the result's data as {action.<block>.<key>}.
Timeline: ACTION_REQUESTED · ACTION_RESULT (outcome, late) · ERROR (ACTION_UNKNOWN, ACTION_PARAMS).
The block is ⚠ when the action is registered as risky.
condition
Branches on a test. Instant: the caller hears nothing.
test.kind |
Other fields | Exits |
|---|---|---|
hours |
— | open · closed (the account's opening hours, in its time zone) |
calledRecently |
days (default 7) |
yes · no: an outgoing call from this account reached this caller in the last days |
variable |
var, op, value |
yes · no |
contactExists |
— | yes · no. Reserved: nothing fills it yet, so it is always no. Don't offer it in the editor |
op for variable (value may contain {variables}; text compares ignore case):
op |
Yes when |
|---|---|
equals (default) · notEquals |
the value is / isn't value |
gt · lt |
greater / less than value (as numbers; 45,000 works) |
in |
one of a comma list: "value": "M-Pesa, Tigo Pesa" |
matches |
matches a regular expression |
exists · empty |
has a value / has none (no value needed) |
Answered by a machine (outgoing):
{"kind": "variable", "var": "answeredBy", "op": "equals", "value": "machine"}.
Timeline: CONDITION (test, result).
goto
Jumps to another block.
| Setting | Type | Default | Rules |
|---|---|---|---|
target |
string | req | The block to jump to (to is accepted too) |
maxLoops |
int | 0 = no limit | After this many jumps from this block in one call → tooMany |
Exits: tooMany. Most editors draw target as a dashed line, not as an exit port.
Timeline: LIMIT (GOTO_LOOPS).
transfer
| Setting | Type | Default | Rules |
|---|---|---|---|
script · version |
string · int | req | A SHARED flow and a runnable version (pinned: changing the shared flow changes nothing here until the pin moves) |
inputs |
object | — | {name: template}: every input the shared flow declares |
Exits: the names the shared flow's Return blocks use (done, failed…). Variables set inside stay visible after.
At most 5 transfers deep.
Timeline: TRANSFER_IN · TRANSFER_OUT (exit).
return
| Setting | Type | Rules |
|---|---|---|
exit |
string | req. [a-zA-Z][a-zA-Z0-9_]{0,30} |
Exits: none.
optout
Adds the caller to the account's do-not-call list (2E): call-outs never call them again.
| Setting | Type | Default |
|---|---|---|
msg |
message | "Umeondolewa kwenye orodha. Hutapigiwa tena." / "You have been removed from the list. We will not call again." |
Exits: next. Adds the tag opted_out.
Timeline: OPTED_OUT.
goodbye
Ends the call.
| Setting | Type | Default |
|---|---|---|
msg |
message | "Asante kwa kupiga simu. Kwaheri." / "Thank you for calling. Goodbye." |
Exits: none.
5.6 The Waiting Room
wr on Connect, Send request and App action: what the caller hears and can do while the block is busy.
"wr": {
"start": { "src": "text", "text": "Tafadhali subiri, tunakuunganisha. Bonyeza tisa kuacha ujumbe." },
"music": "hold_default",
"updates": [ { "src": "text", "text": "Bado tunakutafutia mhudumu." } ],
"updateEverySec": 30,
"queuePosition": true,
"options": [ { "key": "9", "exit": "vm" } ],
"optionsAfterSec": 20,
"giveUpSec": 180
}
("Please wait, we're connecting you. Press nine to leave a message." · "We're still finding you an agent.")
| Field | Default | Description |
|---|---|---|
start |
— | Played once when waiting starts |
music |
hold_default |
An audio id of the account, looped |
updates |
— | Messages played in turn, one every updateEverySec |
updateEverySec |
30 | |
queuePosition |
false | Connect only: "Wewe ni wa 3 kwenye foleni." ("You are number 3 in the queue.") with each update |
options |
— | [{key, exit}]: a key the caller can press to leave through that exit (voicemail, call me back, back to the menu). Key 0–9, *, #. Each exit becomes an exit of the block |
optionsAfterSec |
0 | Keys work only after this many seconds |
giveUpSec |
300 | 10–1800, and never above the account's longest wait. Then noAnswer (Connect), timeout (Send request, App action) |
The Waiting Room says only what the author writes. Announce the option keys in start or updates; options
with neither get the warning WAIT_OPTIONS_UNANNOUNCED.
5.7 The AI Agent
"assistant": {
"type": "ai_agent",
"agent": {
"goal": "Book an appointment",
"persona": "Mhudumu mpole wa Kliniki ya Afya Bora",
"guard": "Never promise prices that are not in the knowledge base",
"voice": "marin", "maxMin": 5,
"language": { "mode": "AUTO", "default": "sw" },
"kb": { "items": [ { "q": "Saa za kazi", "a": "Jumatatu hadi Jumamosi, saa mbili asubuhi hadi saa kumi jioni." } ],
"docs": ["4b1e…"], "site": "https://example.co.tz/huduma" },
"fields": [ { "name": "patient_name", "desc": "Jina la mgonjwa" }, { "name": "date", "desc": "Siku ya miadi" } ],
"tools": [ { "type": "request", "name": "book_slot", "req": "book" },
{ "type": "app action", "name": "send_confirmation", "action": "send_sms", "params": { "text": "Miadi yako ni {ai.date}" } } ],
"canSchedule": { "script": "callback", "version": 1 },
"start": "s1",
"anytime": [ { "kind": "person", "to": "s5" }, { "kind": "upset", "to": "s5" } ],
"stages": {
"s1": { "type": "greet", "say": "Habari! Nikusaidie nini leo?", "leave": [ { "kind": "always", "to": "s2" } ] },
"s2": { "type": "understand", "leave": [ { "kind": "question", "to": "s3" }, { "kind": "intent", "to": "s4" } ] },
"s3": { "type": "answer", "leave": [ { "kind": "always", "to": "s2" } ] },
"s4": { "type": "collect", "collect": ["patient_name", "date"], "leave": [ { "kind": "done", "to": "s6" } ] },
"s6": { "type": "act", "actions": ["book_slot", "send_confirmation"],
"leave": [ { "kind": "success", "to": "exit:goal" }, { "kind": "failed", "to": "s5" } ] },
"s5": { "type": "handover", "ho": {
"ok": { "say": "Nakuunganisha na mhudumu sasa hivi.", "to": "exit:human" },
"busy": { "say": "Wahudumu wote wako kwenye simu, subiri kidogo.", "to": "exit:human" },
"none": { "say": "Tutakupigia ndani ya saa moja.", "to": "exit:cb" },
"closed": { "say": "Tumefunga sasa, tafadhali acha ujumbe.", "to": "exit:vm" } } }
}
},
"x": { "goal": "bye", "human": "desk", "vm": "vm", "cb": "later", "nohelp": "bye", "time": "bye", "err": "desk" }
}
(The Swahili reads: "A kind assistant of Afya Bora Clinic" · "Working hours" · "Monday to Saturday, 8 a.m. to 4 p.m." · "Patient's name" · "Day of the appointment" · "Your appointment is on {ai.date}" · "Hello! How can I help you today?" · "I'm connecting you to an agent now." · "All agents are on calls, please wait a little." · "We'll call you within an hour." · "We're closed now, please leave a message.")
| Field | Default | Description |
|---|---|---|
goal |
"help the caller" | What the conversation is for |
persona · guard |
— | Who the AI is; what it must never do |
voice · speed · model |
engine's | The realtime voice and model |
maxMin |
5 | 1–30 minutes, then the time exit |
language |
the call's | {"mode": "FIXED", "languages": ["sw"]} or {"mode": "AUTO", "default": "sw"} (follows the caller; sets {lang}) |
kb |
— | Knowledge: items (Q/A), docs (ids from 2B, F1), site (an https page, read when the version is pushed). Used only in answer stages; the AI says it doesn't know rather than invent |
fields |
— | [{name, desc}]: details to collect; each becomes {ai.<name>} |
tools |
— | [{type, name, …}]. name: lower case, digits, _. Types: request + req (a Send request block of this flow, reused) · app action + action + params · transfer + department |
canSchedule |
— | {script, version}: lets the AI book a later call (same rules as Schedule call) |
start |
req | The first stage |
anytime |
— | Leave-when rows checked in every stage |
stages |
req | Stage id → stage |
Stage:
| Field | Description |
|---|---|
type |
greet · understand · answer (from knowledge) · collect · act · confirm · handover · custom |
say |
Said on entering the stage |
instruction |
Extra instruction for this stage |
collect |
collect stages: field names (declared in fields) |
actions |
act stages: tool names |
leave |
[{kind, to}]. to: a stage id, or exit:<goal|human|vm|cb|nohelp|time|err> |
ho |
handover stages: {ok, busy, none, closed}, each {say, to}: chosen from the live state of the department behind the human exit (someone free / all busy / nobody online / closed). Defaults: ok, busy → human; none → cb; closed → vm |
Leave-when kinds: always (then) · question (asks a question) · intent (wants to book or act) · done (all
fields collected) · success · failed (an act stage's tools) · person (asks for a person) · upset.
After the conversation: the transcript (by speaker), a summary and the collected fields → summary.ready.
Timeline: AI_VOICE_DISCLOSURE · AI_AGENT_START · AI_STAGE · AI_HEARD · AI_SAID · AI_TOOL · AI_KNOWLEDGE · AI_FIELD · AI_HANDOVER · AI_EXIT · AI_ERROR · AI_ENDED · AI_USAGE.
Rule of thumb: if the AI needs a result to keep talking (check a balance, book a slot), make it a tool. If the AI's part is finished (transfer, pay, goodbye), use an exit to a block.
5.8 What the engine adds by itself
Not settings: don't add blocks for these.
- AI-voice notice: a call that will play generated voice starts with one line, "Habari, unazungumza na huduma ya kidijitali ya {business}." ("Hello, you're speaking with {business}'s digital service."), worded per account. Skipped when the call plays only uploaded audio.
- Recording notice: once per call, before the first recorded part, when the account records.
- "Sorry" and goodbye: if something breaks during a call, the caller hears "Samahani, kuna tatizo kwa sasa…"
("Sorry, there's a problem right now…") and the call ends (
SYSTEM_ERROR). The caller never hears silence. - Retry lines: "Chaguo hilo si sahihi." ("That option is not valid.") for a wrong key; "Samahani, sijaelewa. Unaweza kusema: …" ("Sorry, I did not understand. You can say: …") with the option labels; "Namba uliyoingiza si sahihi." ("The number you entered is not valid.") in Collect; "Samahani, sikusikia vizuri. Tafadhali sema tena." ("Sorry, I did not hear that well. Please say it again.") in Ask question.
5.9 Check codes
POST /v1/scripts/validate and a push return errors and warnings, each {block, code, message}. block is
the block id, or null for the whole flow. An error stops the push; a warning doesn't. Show each on its block; the
message is written to be shown as it is.
| Code | Level | Block / field | How the author fixes it |
|---|---|---|---|
DIRECTION_MISSING |
error | flow | Choose INCOMING, OUTGOING or SHARED |
DIRECTION_MISMATCH |
error | flow | A flow keeps its direction: make a new flow instead |
START_MISSING · START_UNKNOWN |
error | flow | Connect the start to a block |
TOO_MANY_BLOCKS |
error | flow | At most 60 blocks: move a part into a SHARED flow |
UNKNOWN_BLOCK_TYPE |
error | block | Engine doesn't know the type (old editor?): delete the block |
VARS_IN_INCOMING |
error | flow | Incoming flows have no call-out values: remove vars |
RETURN_OUTSIDE_SHARED |
error | flow | Return blocks only in SHARED flows |
EXIT_TARGET_MISSING |
error | block | An exit points to a deleted block: reconnect it |
EXIT_UNCONNECTED |
warning | block | The call ends there. Connect it, or leave it if that is meant |
EXIT_UNKNOWN |
warning | block | Not an exit of that block (old option id?): remove it |
RETRIES |
error | Ask blocks | 0–3 |
MESSAGE_MISSING · MESSAGE_SOURCE · MESSAGE_TEXT |
error | message | Write the text or choose audio |
MESSAGE_TOO_LONG |
error | message | At most 500 characters: split into two messages |
MEDIA_MISSING · MEDIA |
error | message, wr.music |
Choose an audio file; it must be READY and belong to the account |
UNKNOWN_VARIABLE |
error | block | The {name} isn't produced by any block or declared: fix the name or add the variable |
MENU_KEYS |
error | menu | 1–10 options |
MENU_KEY |
error | menu | Keys 0–9, * or #, not repeated; an option without a key needs speech on |
MENU_OPTION |
error | menu | Two options with the same id |
MENU_KEY_LANG |
error | menu | An option's lang is sw or en |
OPTION_WITHOUT_WORDS |
warning | menu | With speech on, give the option words, or it can only be picked by key |
SAVE_AS |
error | collect, ask | A variable name: letters, digits, _ |
DIGITS · PATTERN |
error | collect | 1–20 digits, min ≤ max; a valid regular expression |
POLL_ANSWERS |
error | poll | 1–5 |
PICK_SOURCE · PICK_PAGE |
error | pick | A Send request block that waits, or inline items; 1–9 per page |
ASK_LENGTH |
error | ask | 5–60 seconds |
VOICEMAIL_LENGTH |
error | voicemail | 5–120 seconds |
DEPARTMENT |
error | connect | Choose a department of this account |
WAIT_GIVE_UP · WAIT_OPTION |
error | wr |
10–1800 s; each option a key and an exit |
WAIT_OPTIONS_UNANNOUNCED |
warning | wr |
Tell callers about the keys in the start or update message |
REQUEST_URL · REQUEST_HTTPS |
error | request | An https URL with a public host |
REQUEST_METHOD · REQUEST_BODY · REQUEST_MODE · REQUEST_TIMEOUT |
error | request | A listed value; timeout 1–15 s |
REQUEST_AUTH |
error | request | Fill the auth fields for the chosen type |
CALLBACK_URL |
error | request | Callback mode: put {callback_url} in the body |
ACTION |
error | app_action | An action the client app registered, with all its required parameters |
CONDITION_TEST |
error | condition | hours, calledRecently, contactExists or variable |
GOTO_TARGET |
error | goto | Choose the block to jump to |
TRANSFER_TARGET |
error | transfer | A runnable SHARED flow version; fill its inputs |
RETURN_EXIT |
error | return | An exit name |
SCHEDULE_TARGET |
error | schedule, AI canSchedule |
A runnable OUTGOING flow version |
SCHEDULE_TIME |
error | schedule | Exactly one of after / at / ask, in the right form |
SCHEDULE_TRIES |
error | schedule | 1–3 |
AGENT_START · AGENT_MOVE · AGENT_TOOL · AGENT_FIELD · AGENT_KB · AI_LANGUAGE |
error | ai_agent | First stage set; every move goes to a stage or an exit; tools and fields declared; knowledge docs of the account; site https |
FEATURE_OFF |
error | ai_agent, menu/poll/pick with smart |
The client app has no AI feature: use words matching, or remove the AI Agent |
Besides the issues, the check returns:
texts: the fixed texts that will be made into audio;vars: the declared call-out values (for the call-out upload screen);riskyBlocks: the blocks that need approval (2B, D7).
No comments to display
No comments to display