Scripts & Blocks Reference

Client app developers: read api/5-flow-reference.md instead. It is checked against the engine's code. This file is the engine's design notes (worker commands, execution, costs); some names here are older than the code (e.g. Condition test.kind, Go to target).

The one system that decides what happens on incoming calls, call-outs (broadcasts, campaigns) and AI conversations. Built by customers in the client app's Voice Studio (block canvas). Companion to SPEC.md, ENGINE.md, ARCHITECTURE.md, VOICE-WORKER.md, CLIENT-APP-ACTIONS.md, SCENARIOS.md, EXAMPLES.md. SIP trunk only (2026-11-12). Blocks run as commands for the voice worker inside the call's LiveKit room; no AT XML. The Voice API version of this file is in archive/.


1. Concepts

A script is a graph of blocks. Each block does one thing and has named exits wired to the next block. An unconnected exit ends the call (with the account's goodbye if the call is still up).

Script direction Where it's used Start block exits
Incoming Numbers page (one script per number) call · Incoming call
Outgoing Call-outs (broadcast, campaign, API POST /calls) out · Person answered
Shared Only through Transfer to script, from either direction in · Transferred in

One direction per script. To reuse content in the other direction, duplicate the script, or put the shared part in its own script and use Transfer to script.

Not scripts: OTP (system message), click-to-call (live call), callback (a follow-up task).

1.1 Layers

Message type (Incoming / OTP / Broadcast / Campaign / Click-to-call)   → lane, recording rule, limits
  └─ Script version (blocks + settings)                                → what happens
      └─ Block (announcement, prompt, waiting room, exits)             → one step
          └─ Rendering (variables filled, text → cached audio)        → what the caller hears
              └─ Worker commands (play, ask, record, wait, connect, ai) → what the worker does in the room

1.2 How blocks become worker commands

Command Used by
play Say, announcements, AI disclosure, recording notice, Goodbye, system messages
ask Menu, Collect number, Poll, Pick from list, Ask question (keys and/or speech)
record Voicemail
wait Waiting Room: Connect, Send request (wait), App action (wait)
connect / hold Connect once an agent accepted; hold and transfer during the live call
ai AI Agent
hangup Goodbye, Opt out (after its message), limits

Rules (full protocol: VOICE-WORKER.md):


2. Messages

Same shape everywhere:

{ "src": "text",    "text": "Habari {name}", "lang": "sw" }
{ "src": "record",  "media": "med_123" }
{ "src": "upload",  "media": "med_456" }
{ "src": "library", "media": "sys_hold_default" }

(The Swahili example reads "Hello {name}".)


3. Variables and metadata

Variables are typed values that blocks read aloud, send, or test. Written as {name}.

Group Examples Available
System {caller}, {number}, {callId}, {date}, {time}, {key} (last key) Everywhere
Contact / call-out {name}, {amount}, {order}… (declared by the script; filled per person from the client app, a file, or the API) Outgoing scripts
Collected {input.account} from Collect number; {choice.*} from Pick from list; {answer.problem} from Ask question; {heard} (last thing the caller said); {lang}. Spoken, {input.*} and {poll.*} digits are read one by one in words ("moja mbili tatu nne"); requests get the raw digits After that block
Reply {status}, {ref}… mapped from a Send request reply; {action.<block>.*} from an App action result After that block
AI {ai.patient_name}, {ai.date}, {ai.amount}, {ai.language} After the AI Agent
Live facts {business.open}, {business.opens_at}, {agents.online}, {queue.wait} Everywhere (read live)

Metadata: opaque JSON the client attaches to a call or call-out ({"orderId": 991}). Never spoken, never sent to Send request unless mapped into a variable. Echoed back in every event as meta.client.


4. Script document

{
  "scriptId": "scr_debt", "version": 3, "direction": "OUTGOING",
  "settings": { "record": "policy", "maxCallMin": 15, "language": "sw", "voice": { "sw": "marin", "en": "marin" } },
  "vars": [ { "name": "name", "type": "text" }, { "name": "amount", "type": "money" } ],
  "start": { "out": "say_1" },
  "blocks": {
    "say_1":  { "type": "say", "msg": { "src": "text", "text": "Habari {name}. Deni lako ni {amount}." }, "x": { "next": "menu_1" } },
    "menu_1": { "type": "menu", "msg": { "src": "text", "text": "Bonyeza 1 au sema 'lipa' kulipa sasa, 2 kuongea na mhasibu, 9 kujiondoa." },
                "options": [ { "k": "1", "id": "pay",   "label": "Lipa",     "words": ["lipa", "kulipa", "malipo"] },
                             { "k": "2", "id": "staff", "label": "Mhasibu",  "words": ["mhasibu", "mtu", "kuongea"] },
                             { "k": "9", "id": "out",   "label": "Jiondoe",  "words": ["jiondoa", "acha"] } ],
                "speech": { "match": "words" }, "timeoutSec": 8,
                "x": { "pay": "pay_1", "staff": "connect_1", "out": "optout_1", "noInput": "bye_1", "notUnderstood": "menu_1" } },
    "pay_1":  { "type": "app_action", "action": "pay", "params": { "amount": "{amount}", "wallet": "M-Pesa" },
                "wr": { "preset": "Payment" }, "x": { "paid": "bye_2", "failed": "sms_1", "timeout": "sms_1" } }
  }
}

Each block: type, block settings, optional before (announcement message), optional wr (Waiting Room), x (exit → block id; missing = end). (The Swahili lines read: "Hello {name}. Your debt is {amount}." · "Press 1 or say 'pay' to pay now, 2 to talk to the accountant, 9 to opt out.")


5. Blocks (complete list: 17)

⚠ = needs approval (the whole script version).

Speak

Block Settings Exits Command
Say / Play message (one or several parts), interruptible (default off) next play

Ask: keys, speech, or both

All Ask blocks share one rule set:

Block Settings Exits Notes
Menu prompt, options [{k key, id, label, words [spoken words], lang}], speech {match: words | smart, minConfidence}, timeoutSec, retries each option id, noInput, notUnderstood Keys pick by k; speech by words (exact or close), or with smart by the AI (gpt-6-luna), which understands free speech ("nataka kuongea na mtu wa malipo", "I want to talk to someone about payments"). An option with lang (sw, en) also switches the call's language ({lang}). Replaces the AI Router (removed 2026-11-12).
Collect number ⚠ question, save as {input.x}, min/max digits, finish key #, pattern, speech (off by default) ok, failed Keys by default: spoken account numbers are where speech recognition errs most. With speech, spoken numbers ("sifuri saba moja…", "zero seven one…") become digits.
Poll question, answers 1–5 with optional words, save as next, noInput
Pick from list intro, list source (a Send request reply items[], or inline), label template, per page (≤ 9, 0 = more) chosen (sets {choice.id}, {choice.label}, {choice.*}), empty, noInput With speech, the caller can say the item's name.
Ask question (new) question, save as {answer.x}, maxSec (5–60, default 20), language answered, silence "Tell us your problem." The caller's words become text in {answer.x} and the call carries on (Voicemail ends it). Sent to Send request, App actions or the summary.

Connect

Block Settings Exits Command
Connect to department department, record On/Off, Waiting Room. Whether members' mobiles ring too is the number's ringMode (D51), not a block setting noAnswer + Waiting Room option exits wait, then connect
Voicemail prompt, max length (30/60/120 s), beep, finish key — (ends) play + record
Schedule call (new, SCHEDULE-CALL.md) Outgoing script + pinned version; time: after (30m, 1h, 1d), at (17:00, tomorrow 09:00) or ask (the caller says it: "kesho saa tatu asubuhi"); confirm; vars for the later call; tries scheduled, notUnderstood, declined, refused books a one-person call-out; says the time back in Kiswahili time
AI Agent ⚠ see §8 goal, human, vm, cb, nohelp, time, err ai

Integrate

Block Settings Exits
Send request ⚠ method, URL, params, headers, body (JSON / form / raw / none), auth (Signature default, Bearer, Basic, API key, none), reply mapping, timeout, mode Send and continue: next · Wait for reply / Wait for callback: success, failure, timeout (+ Waiting Room options)
App action action (a name the client app registered), params, message before waiting, Waiting Room (if the action waits) Doesn't wait: next · Waits: each registered outcome + failed + timeout (+ Waiting Room options)

App action replaces "Textify action" and "Pay now": the engine has no built-in SMS or payment actions; each client app registers its own (CLIENT-APP-ACTIONS.md). An action the app marks risky (e.g. pay) makes the block ⚠.

Send request details:

Logic

Block Settings Exits
Condition Business hours · Called by us recently (7 days) · Contact exists in the client app · Answered by (outbound: person / machine, from answering-machine detection) · Variable test (equals, not equals, >, <, in, matches, exists, empty). Later: agents online, queue length, caller in a list, percentage split Business hours: open, closed · Answered by: person, machine · others: yes, no
Go to target block, maxLoops (default 3) — (jumps; after maxLoops jumps from this block in one call → its tooMany exit, or the call ends)
Transfer to script (new) script ref + pinned version, inputs {name: template} the target's return exits (e.g. done, failed)

Transfer to script works like a function call into a shared script (D52):

End

Block Settings Exits Notes
Opt out confirmation message next Adds {caller} to the account's do-not-call list
Return exit (name) — Shared scripts only: back to the Transfer block, through its exit of that name
Goodbye message — Ends the call

5a. Outcome tag & sensitive flag (any block)

6. Announcement (any block)

Automatic, not editable per block:


7. Waiting Room (Connect, Send request "wait", App action "wait")

What the caller hears and can do while the block is busy. It runs in the call's room: the worker plays it, the engine decides when it ends.

Setting Meaning
Preset Queue (Connect) · Quick check (Send request) · Payment (App action) · Custom
Start message Plays once when waiting starts ("Tafadhali subiri…", "Please wait…")
Background Hold music (library or upload)
Updates every N s Rotating messages ("Bado tunashughulikia…", "We're still working on it…")
Queue position Connect only: "Wewe ni wa 3 kwenye foleni" ("You are 3rd in the queue"), refreshed when it changes
Offer options after N s Keys → exits: Voicemail, Call me back, Back to menu… (appear as ⏳ exits on the block)
Give up after N s → noAnswer / timeout / failed exit

How it runs:

engine → worker: wait { start, music (loop), messages [{audio, everySec}], keys allowed after N s }
worker: plays start, loops music, plays updates; keys → result
engine, when something changes:
  work done (agent accepted, reply arrived, outcome reported) → wake worker → next block's commands
  queue position changed                                       → wake worker → new wait (fresh position message)
  give-up timer (JobRunr) fires                                → wake worker → give-up exit
  option key (allowed only after "offer options after N s")    → that option exit

8. AI

8.0 AI language

Each AI Agent, and each Ask block with speech, has a language setting chosen by the customer:

"language": { "mode": "FIXED", "languages": ["sw"] }      // or { "mode": "AUTO", "default": "sw" }

The detected language is saved as {ai.language} (and on the call), so later blocks and the client app know it. With AUTO and sw/en it also sets {lang}, so later system messages follow the caller.

8.1 Smart matching (Ask blocks)

8.2 AI Agent

Designed in the Agent canvas (double-click the block).

{
  "goal": "Book an appointment", "persona": "…", "voice": "marin",
  "guard": "Never promise prices not in the knowledge base", "maxMin": 5,
  "kb": { "items": [ { "q": "Saa za kazi", "a": "…" } ], "docs": ["bei_2026.pdf"], "site": "https://…" },
  "fields": [ { "name": "patient_name", "type": "Text", "desc": "Jina lako ni nani", "req": "Required" },
              { "name": "date", "type": "Date", "req": "Required" } ],
  "tools": [ { "type": "Use saved request", "name": "book_appointment", "req": "request_1" },
             { "type": "App action", "name": "send_confirmation", "action": "send_sms", "params": { "text": "Miadi yako ni {ai.date}" } } ],
  "start": "s1",
  "anytime": [ { "kind": "person", "to": "s7" }, { "kind": "upset", "to": "s7" } ],
  "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", "kb": true, "leave": [ { "kind": "always", "to": "s2" } ] },
    "s4": { "type": "collect", "collect": ["patient_name", "date"], "leave": [ { "kind": "done", "to": "s5" } ] },
    "s5": { "type": "act", "actions": ["book_appointment", "send_confirmation"], "leave": [ { "kind": "success", "to": "s6" }, { "kind": "failed", "to": "s7" } ] },
    "s6": { "type": "confirm", "leave": [ { "kind": "always", "to": "exit:goal" } ] },
    "s7": { "type": "handover", "ho": {
              "ok":     { "say": "Nakuunganisha na mhudumu sasa hivi.", "to": "exit:human" },
              "busy":   { "say": "Wahudumu wote wako kwenye simu…",     "to": "exit:human" },
              "none":   { "say": "Tutakupigia ndani ya saa moja.",       "to": "exit:cb" },
              "closed": { "say": "Tumefunga sasa… acha ujumbe.",         "to": "exit:vm" } } }
  }
}

(The Swahili reads: "What are your working hours" · "What is your name" · "Your appointment is on {ai.date}" · "Hello! How can I help you today?" · "I'm connecting you to an agent right now." · "All agents are on calls…" · "We'll call you back within an hour." · "We're closed now… leave a message.")

Part Meaning
Stage types Greet, Understand, Answer from knowledge, Collect details, Take action, Confirm, Handover, Custom
Stage settings instruction, says on entering, fields to collect, allowed actions, may answer from knowledge, leave when rows
Leave-when kinds Then (always), Asks a question, Wants to book/act, All info collected, Action succeeded, Action failed, Asks for a person, Caller upset
Anytime rules Checked every turn from any stage (asks for a person, caller upset)
Knowledge base Inside the agent: facts/FAQs, documents, website page. Used only in stages that allow it. Never invent
Fields Become {ai.*} variables for later blocks
Tools Use saved request (reuses a Send request block of the same script: URL, auth, reply mapping), App action (any action the client app registered; a waiting action's outcome is the tool result), Transfer to department. The AI uses the result to keep talking
Live facts {business.open}, {business.opens_at}, {agents.online}, {queue.wait} from the system
Handover Checks live facts: open + free → human · open + busy → human (queue) · no agents → call me back · closed → leave a message
Exits goal, human, vm (leave a message), cb (call me back), nohelp, time, err: ordinary exits that may lead to any block

How it runs:

Rule of thumb for tools vs exits: if the AI needs the result to keep talking (check balance, book) → a tool inside the agent; if the AI's part is finished (transfer, pay, goodbye) → an exit to a block.


8a. Block settings (exact JSON)

Block Settings
say msg (message or list), interruptible
menu msg, options [{k, id (default = k), label, words, lang}], speech {match: words | smart, minConfidence, language} or absent, timeoutSec (8), retries (2)
collect msg, saveAs → {input.<saveAs>}, minDigits/maxDigits (1–20), finishKey (#), pattern (regex), sensitive, retries, timeoutSec, speech (bool, off; never with sensitive)
poll msg, answers (1–5), saveAs → {poll.<saveAs>} (default block id), labels [{k, label, words}], speech
pick source (id of a request block that waits for its reply, or inline + items), msg (intro), label template over item fields ({label} default), perPage 1–9 (5), speech
ask msg, saveAs → {answer.<saveAs>}, maxSec (5–60, 20), language, useMenuWords (bool, off: when the Menu just before was answered by voice in 6+ words, those words are the answer and the question is skipped)
connect department, record, wr
schedule script + version (Outgoing), exactly one of after / at / ask {msg}, confirm (true with ask), vars {name: template} → the later call (plus {scheduled.said}, {scheduled.from}, {scheduled.note}), note, maxTries (1–3, 2), retryGapSec (≥ 300), msg (after booking; may use {schedule.when}). Rules: 5 min–30 days ahead, quiet hours move it (and the read-back says so), a new booking for the same caller and script replaces the old one, at most 3 pending per caller
Ask blocks' later exit Menu, Collect, Poll, Pick: words meaning "not now" (baadaye, busy, later, sina muda, niko kazini…, plus laterWords) take later when it is wired; usually to a Schedule call
voicemail msg, maxSec (30/60/120), beep (true: one beep to start; two short beeps warnBeforeEndSec before the limit; one long beep when time is up. Only the caller hears them, they are not in the recording), warnBeforeEndSec (default 10, or a third of a short maxSec; 0 = no warning), finishKey
request url (https; {vars} URL-encoded), method (POST default), params {name: template}, headers {name: template} (X-Voice-*, Host, Content-* ignored), body {type: json (default) | form | raw | none, value}, auth {type: signature (default) | bearer token | basic username+password | apikey header+value | none}, map {"json.path[0]": "varName"}, timeoutSec 1–15 (10), mode continue | reply (default) | callback, wr
app_action action, params {name: template}, msg (spoken before waiting), wr
condition test: hours | recent | contact | answeredBy | var {var, op, value}
goto to, maxLoops
transfer script (ref), version, inputs {name: template}
return exit
optout, goodbye msg

Behaviour:


9. Validation (on save / before publish)

Loops are allowed: any exit may point back to an earlier block or to the same block ("press 0 to repeat", story → back to the menu). The loop guard ends the call (LOOP_GUARD) only after maxBlockVisits (25) blocks in a row without the caller pressing a key, speaking or recording, so a broken say → goto → say loop stops, while a caller going round a menu is never cut off. Call length is capped separately (maxCallMinutes).

  1. Start exits exist for the direction; every exit points to an existing block or is intentionally empty.
  2. ≤ 60 blocks; menus ≤ 10 options; retries ≤ 3; text ≤ 500 characters.
  3. Variables exist in scope: call-out variables only in Outgoing scripts; {input.x} after its Collect; {answer.x} after its Ask question; reply variables after their Send request; {ai.*} after the AI Agent.
  4. Media READY; departments and saved requests belong to the account; App actions exist in the client app's list with every required parameter (CLIENT-APP-ACTIONS.md §4).
  5. Send request: HTTPS URL, valid JSON body, auth complete.
  6. Speech: a Menu, Poll or Pick with speech and match: words needs words on every option (warning OPTION_WITHOUT_WORDS); a sensitive Collect can't listen for speech.
  7. Transfer to script: the target version exists, is runnable and SHARED; every declared input is filled; the block's exits match the target's return exits; no cycle; nesting ≤ 5. Return blocks only in SHARED scripts.
  8. AI Agent: start stage set, every leave-when connected or intentionally empty, at least one exit reachable.
  9. Approval flag computed: any ⚠ block → version status Waiting approval; else Ready.
  10. Text to audio: every fixed text is listed in the version's texts so its audio is made at publish (ARCHITECTURE §5.2).

10. Versioning & approval


11. Execution (engine)


12. Cost per block

Block Cost
Any text message TTS once per distinct text (cached); nothing per call after that
Say/Play, Menu, Collect, Poll, Pick, Voicemail, Goodbye Call minutes + SIP trunk on AT
Speech in an Ask block + transcription of the answer (seconds); smart matching adds a small model call
Ask question + transcription of the answer
Connect (waiting and talking) Minutes + trunk
Send request, Condition, Go to, Transfer to script, Opt out Free
App action Whatever the client app charges for it (SMS, payment fee); nothing on the engine
AI Agent Minutes + trunk + OpenAI Realtime usage

Revision #1
Created 13 November 2026 19:03:09 by Admin Qbit
Updated 13 November 2026 19:03:44 by Admin Qbit