@talqing/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/SKILL.md +1270 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +117 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
- package/tools.json +5772 -0
package/SKILL.md
ADDED
|
@@ -0,0 +1,1270 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: talqing
|
|
3
|
+
description: >-
|
|
4
|
+
Build, publish and operate Talqing AI voice, video and text agents — agent
|
|
5
|
+
config and model stack, custom tools and their operation trees, MCP and
|
|
6
|
+
messaging integrations, knowledge bases, phone numbers, secrets and webhooks,
|
|
7
|
+
plus placing calls and reading back transcripts, cost and latency. Use
|
|
8
|
+
whenever working against a Talqing workspace through the talqing MCP server.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<!-- Generated by openapi/export.py from backend/services/copilot/skill.md. Edit that file, then re-run the export. -->
|
|
12
|
+
|
|
13
|
+
# Building agents on Talqing
|
|
14
|
+
|
|
15
|
+
Talqing is a no-code builder for AI voice, video and text agents. Everything you
|
|
16
|
+
can build here you could otherwise have written as a LiveKit agent by hand: a
|
|
17
|
+
model stack, a system prompt, callable tools, knowledge, and lifecycle hooks.
|
|
18
|
+
The same machinery also runs **agent tasks** — an agent nobody talks to, which
|
|
19
|
+
takes named inputs and returns a typed result.
|
|
20
|
+
|
|
21
|
+
The same surface takes an agent all the way to production: give it a phone
|
|
22
|
+
number, run it, and read back every call it handled.
|
|
23
|
+
|
|
24
|
+
You reach the platform through one operation per API endpoint. Each operation's
|
|
25
|
+
arguments mirror its HTTP call — path and query parameters at the top level, the
|
|
26
|
+
request body under `body`. Every operation acts as the signed-in user, inside
|
|
27
|
+
their workspace only, with their role's permissions. Managing who is in the
|
|
28
|
+
workspace is the one thing that stays in the dashboard.
|
|
29
|
+
|
|
30
|
+
## What an agent is
|
|
31
|
+
|
|
32
|
+
An agent is a single object, `config`, plus what you attach to it:
|
|
33
|
+
|
|
34
|
+
- **`name`** — unique in the workspace.
|
|
35
|
+
- **`channel`** — `voice` (phone/web call), `video` (a voice agent wearing an
|
|
36
|
+
Anam avatar) or `text` (chat). The channel decides which model slots exist.
|
|
37
|
+
- **`prompt`** — the system prompt. This is where the agent's behaviour lives.
|
|
38
|
+
- **`greeting`** — the line spoken on connect. Voice and video only.
|
|
39
|
+
|
|
40
|
+
Both are personalized per session: `{{userdata.field}}` is substituted from
|
|
41
|
+
the session's userdata when the agent starts, so *"You are speaking with
|
|
42
|
+
{{userdata.name}}, a {{userdata.tier}} customer"* becomes a real sentence on
|
|
43
|
+
the call. A field that is not in userdata resolves to nothing, so write the
|
|
44
|
+
prompt so it still reads if one is absent. `{{args.…}}` and `{{secrets.…}}`
|
|
45
|
+
are rejected here — those belong to a tool call.
|
|
46
|
+
|
|
47
|
+
A token whose root is not one of ours is an **error**, not text: a bare
|
|
48
|
+
`{{name}}`, a `{{customer.number}}` pasted from another platform, or a typo
|
|
49
|
+
like `{{userdate.name}}` is refused when the agent is saved. Rewrite each one
|
|
50
|
+
onto a real root rather than leaving it in.
|
|
51
|
+
|
|
52
|
+
Two more roots are filled in from outside the session, and nothing inside one
|
|
53
|
+
can write either. They differ in who fills them.
|
|
54
|
+
|
|
55
|
+
**`{{system_vars.…}}` — ours.** Six keys, and any other is rejected at save;
|
|
56
|
+
`GET /v1/catalog` returns them as `system_vars`.
|
|
57
|
+
|
|
58
|
+
- `{{system_vars.human_phone_number}}` — the other party, the caller inbound
|
|
59
|
+
and the person you called outbound
|
|
60
|
+
- `{{system_vars.agent_phone_number}}` — your number on this call
|
|
61
|
+
- `{{system_vars.direction}}` — `inbound` or `outbound`
|
|
62
|
+
- `{{system_vars.now}}` — `2026-08-19T19:26:59+05:30`, the machine format;
|
|
63
|
+
send it to an API, never read it aloud
|
|
64
|
+
- `{{system_vars.date}}` — `Wednesday, 19 August 2026`
|
|
65
|
+
- `{{system_vars.time}}` — `7:26 PM`
|
|
66
|
+
|
|
67
|
+
The first three come from the call, so they are empty on web calls and on text
|
|
68
|
+
conversations — do not make one the only source of a fact the agent needs. One
|
|
69
|
+
agent can greet a caller and someone it called differently by branching on
|
|
70
|
+
`{{system_vars.direction}}`. The last three come from the agent's
|
|
71
|
+
**`timezone`** and work on every channel; using one without setting `timezone`
|
|
72
|
+
is a save error. In a prompt or greeting the clock is frozen at the moment the
|
|
73
|
+
agent started (re-resolving would throw away the model's prompt cache); inside
|
|
74
|
+
a tool it is the moment the tool ran.
|
|
75
|
+
|
|
76
|
+
**`{{vars.…}}` — the tenant's.** The agent's own **`vars`** declares them, and
|
|
77
|
+
the request that starts a session supplies values that override the declared
|
|
78
|
+
defaults. See **`vars`** below.
|
|
79
|
+
- **`llm`, `stt`, `tts`** — `{provider, model}` from the provider catalog. Text
|
|
80
|
+
agents have only `llm`; voice and video need all three.
|
|
81
|
+
- **`language`** — the one language the caller and the agent speak, shared by
|
|
82
|
+
every model. `null` is Auto. Voice and video only.
|
|
83
|
+
- **`timezone`** — an IANA name such as `Asia/Kolkata`. What the three
|
|
84
|
+
`{{system_vars.…}}` clock variables resolve against, on every channel. `null`
|
|
85
|
+
until one of them is used, and then required. Set it whenever the agent has to
|
|
86
|
+
reason about opening hours, "today", or an appointment time.
|
|
87
|
+
- **`vars`** — the variables this agent reads as `{{vars.name}}`, each
|
|
88
|
+
`{name, description, default, required}`. Names are `[A-Za-z_][A-Za-z0-9_]*`
|
|
89
|
+
and unique within the agent; `default` is a string or `null`.
|
|
90
|
+
|
|
91
|
+
Values arrive on the **request that starts the session** — `vars` on
|
|
92
|
+
`calls_token`, `create_outbound_call`, `create_call_batch` or
|
|
93
|
+
`create_text_conversation` — and override the defaults. One bag per session,
|
|
94
|
+
reaching *every* agent on it: the entry agent, every team member, and a handoff
|
|
95
|
+
target that was never in the plan, each merged over its own declared defaults.
|
|
96
|
+
Nothing inside the session can write it.
|
|
97
|
+
|
|
98
|
+
Four things to get right:
|
|
99
|
+
|
|
100
|
+
- **They are visible to the model.** Anything in `vars` may be read aloud or
|
|
101
|
+
repeated back. A credential belongs in a workspace secret, read as
|
|
102
|
+
`{{secrets.NAME}}` from a tool, where the model never sees it.
|
|
103
|
+
- **`required: true` refuses the session** that supplies no value for a
|
|
104
|
+
variable with no `default` — the call token, the dial, the batch and the text
|
|
105
|
+
conversation, before anything is compiled and before any provider is called.
|
|
106
|
+
An empty string counts as a value; a `default` satisfies it outright. Nothing
|
|
107
|
+
is enforced once a session is connected, so a handoff target that needs a
|
|
108
|
+
value nobody supplied reads it as empty rather than ending the call.
|
|
109
|
+
- **An inbound call or message has no request of ours**, so only the declared
|
|
110
|
+
defaults resolve — which makes the rule above a hard one for inbound voice: a
|
|
111
|
+
voice agent that requires a variable with no default cannot answer a phone
|
|
112
|
+
number at all, and assigning the number, publishing that config under one
|
|
113
|
+
already assigned, and rolling back to it are all refused. Anything an inbound
|
|
114
|
+
session needs has to have a default.
|
|
115
|
+
- **`agent_override.vars` means something different**: it changes an agent's
|
|
116
|
+
*declarations and defaults* for one call. The top-level `vars` on the same
|
|
117
|
+
request supplies *values*. Both can appear on one call and they do not
|
|
118
|
+
conflict — the override decides what is declared, the top-level bag what it
|
|
119
|
+
is worth.
|
|
120
|
+
|
|
121
|
+
Reading `{{vars.x}}` where `x` is not declared is a **warning**, not an error:
|
|
122
|
+
the key space is open, because an agent or a tool defined inline in the same
|
|
123
|
+
request may legitimately read one nothing declared beforehand.
|
|
124
|
+
|
|
125
|
+
Prefer `userdata` for facts about the *person* and `vars` for configuration of
|
|
126
|
+
the *session*. `userdata` is merged onto the caller's contact record, replayed
|
|
127
|
+
into their next conversation, and writable by any tool; `vars` is none of those.
|
|
128
|
+
- **`avatar`** — video only: `{provider, model, avatar_id, name}`.
|
|
129
|
+
- **`turn_handling`** — turn detection, endpointing, interruption and preemptive
|
|
130
|
+
generation. Voice and video only.
|
|
131
|
+
- **`background_audio`** — ambient and thinking sounds mixed into the agent's
|
|
132
|
+
output. Any transfer stops the ambient bed while the caller waits and plays
|
|
133
|
+
hold music instead, so `ambient: hold_music` only sets how loud that is.
|
|
134
|
+
- **`noise_cancellation`** — cleans up the caller's audio before the agent hears
|
|
135
|
+
it: `{enabled, provider, model, enhancement_level}`, `provider`/`model` from
|
|
136
|
+
the catalog. Off unless asked for, and it needs that provider's key under
|
|
137
|
+
BYOK. `enhancement_level` runs 0.0–1.0; 0.5 is conservative, 0.8 is the
|
|
138
|
+
default, 1.0 suppresses interfering speech hardest. Voice and video only.
|
|
139
|
+
- **`tools`** — published tools the agent's LLM may call mid-conversation, each
|
|
140
|
+
`{"tool_id": "...", "tool_version": null}`. Write `tool_version` as null (or
|
|
141
|
+
leave it out): a draft tracks whatever is published now, and publishing fills
|
|
142
|
+
the version in.
|
|
143
|
+
|
|
144
|
+
An entry may instead carry `{"tool": {name, description, json_schema,
|
|
145
|
+
operations, …}}` — a whole tool written inline. On an agent that is a
|
|
146
|
+
*shorthand*, not a second kind of tool: `create_agent` and `update_agent`
|
|
147
|
+
create it, publish v1 and store `{"tool_id": "..."}` in its place, so the
|
|
148
|
+
agent and its tools can be built in one request. Use it for exactly that.
|
|
149
|
+
Attaching an existing tool is still `tool_id`.
|
|
150
|
+
- **`kb_ids`** — ready knowledge bases for grounded answers.
|
|
151
|
+
- **`mcps`** — external MCP servers whose tools the agent may call, each
|
|
152
|
+
`{"integration_id": "..."}` naming an active, MCP-capable integration. Which
|
|
153
|
+
of a server's tools may be called is the integration's own `allowed_tools`,
|
|
154
|
+
not this list. As with `tools`, an entry may instead carry `{"mcp": {name,
|
|
155
|
+
url, headers, allowed_tools}}`, which is created as a `custom_mcp` integration
|
|
156
|
+
and replaced by its id.
|
|
157
|
+
- **`handoffs`** — where this agent may pass the conversation next, each
|
|
158
|
+
`{"name": "Billing", "agent_id": "...", "description": "...", "context":
|
|
159
|
+
"transcript" | "summary" | "none", "recent_turns": 2, "summary_prompt": "...",
|
|
160
|
+
"message": "..."}`. The compiler turns each entry into one tool the model can
|
|
161
|
+
call, `handoff_to_<name>`, and the model routes on `description` — so write
|
|
162
|
+
that as what belongs there ("invoices, refunds, payment questions"), not as an
|
|
163
|
+
instruction.
|
|
164
|
+
|
|
165
|
+
**This is how you build a multi-agent flow.** Do not create a one-node tool
|
|
166
|
+
per edge: three agents routing to each other is three agents with `handoffs`
|
|
167
|
+
on them, not three agents plus six published tools. The `handoff` *operation*
|
|
168
|
+
is still right for a CONDITIONAL handoff — look the account up, and *if* it is
|
|
169
|
+
enterprise, hand to the enterprise desk — because that decision belongs in an
|
|
170
|
+
operation tree rather than to the model.
|
|
171
|
+
|
|
172
|
+
`agent_id` names a stored agent, entered at its latest published version.
|
|
173
|
+
Leaving it out resolves `name` against the team defined on the call that runs
|
|
174
|
+
the agent, which publishes with a warning rather than an error.
|
|
175
|
+
|
|
176
|
+
**`context` is what the next agent starts from** — the same three words an
|
|
177
|
+
agent's own `conversation.context` uses for what a new *call* starts from.
|
|
178
|
+
|
|
179
|
+
- `transcript` (the default) hands over everything said so far. Nothing to
|
|
180
|
+
configure, and the target pays for all of it on every turn it then takes.
|
|
181
|
+
- `summary` asks THIS agent to write the summary itself, as an argument on the
|
|
182
|
+
`handoff_to_*` tool, in the same turn as the decision to hand over. There is
|
|
183
|
+
no second LLM call, so a realtime agent can use it too. It costs a pause
|
|
184
|
+
before the handoff — the agent writes 60–100 tokens before the tool returns
|
|
185
|
+
— and buys a short, focused context that the source agent chose, instead of
|
|
186
|
+
forty turns the target has to re-read every turn.
|
|
187
|
+
- `none` starts the target with only its own instructions.
|
|
188
|
+
|
|
189
|
+
**`recent_turns`** (1–10) is how many recent turns cross *verbatim* alongside
|
|
190
|
+
the summary, so the target knows what is being asked right now. A turn starts
|
|
191
|
+
at a user message and runs until the next one, so a turn that took a tool call
|
|
192
|
+
and two replies crosses whole. Tool calls themselves never cross — their
|
|
193
|
+
results are knowledge, and knowledge belongs in the summary.
|
|
194
|
+
|
|
195
|
+
Leave it out and each policy answers for itself: `summary` carries two turns,
|
|
196
|
+
and `none` carries none. Under `summary` the tail can be sized down to one turn
|
|
197
|
+
but **not** switched off — a summary with no tail leaves the target knowing the
|
|
198
|
+
history and not the question. Under `none` it is opt-in, and `{"context":
|
|
199
|
+
"none", "recent_turns": 3}` is a real and useful combination: the last three
|
|
200
|
+
turns and nothing else. Past ten turns the honest answer is `transcript`.
|
|
201
|
+
|
|
202
|
+
**`summary_prompt`** replaces the platform's default line with your own. It is
|
|
203
|
+
the *description of the tool argument*, so it is literally what the agent is
|
|
204
|
+
asked to write. It takes `{{userdata.…}}`, `{{system_vars.…}}` and
|
|
205
|
+
`{{vars.…}}` like the description and the message do. The default is one line
|
|
206
|
+
naming three buckets — what the caller wants, what has been done, what is still
|
|
207
|
+
open — and a longer one buys a longer pause before the handoff.
|
|
208
|
+
|
|
209
|
+
`recent_turns` and `summary_prompt` are **rejected** where they do not apply,
|
|
210
|
+
rather than ignored: no `recent_turns` under `transcript`, which already
|
|
211
|
+
carries every turn, and no `summary_prompt` outside `summary`.
|
|
212
|
+
|
|
213
|
+
**`summary` is not a privacy boundary, and must not be described as one.** It
|
|
214
|
+
narrows what the target model is shown; it guarantees nothing. The tail always
|
|
215
|
+
crosses. `userdata` crosses regardless — it is session state every agent on the
|
|
216
|
+
call shares. The full transcript is still recorded and still displayed. A later
|
|
217
|
+
`transcript` hop shows that agent the whole call, including the part `summary`
|
|
218
|
+
scoped away. And the summary is written by an LLM the caller has been talking
|
|
219
|
+
to, so it is exactly as trustworthy as anything else the model says. The one
|
|
220
|
+
thing that does hold is that the source agent's raw tool results do not cross.
|
|
221
|
+
- **`conversation`** — what a new call knows about earlier ones with the same
|
|
222
|
+
person: `context` is `none` (the default — every call starts clean, though
|
|
223
|
+
earlier conversations are still saved against that caller), `summary` (the
|
|
224
|
+
agent is told, as background, what happened on recent ones) or `transcript`
|
|
225
|
+
(one long conversation across every call). `summary_limit` bounds how many
|
|
226
|
+
recent calls `summary` describes — leave it out for all of them — and
|
|
227
|
+
`initialize_userdata` decides whether the call also starts from what the agent
|
|
228
|
+
learned about this person before.
|
|
229
|
+
|
|
230
|
+
This is the answer whenever someone wants an agent to remember, or to forget,
|
|
231
|
+
earlier calls. **Never write that instruction into the prompt** — a model told
|
|
232
|
+
to ignore what is in its context is being asked to do the impossible, and a
|
|
233
|
+
model told to remember cannot see what was never loaded. Set the field.
|
|
234
|
+
`summary` needs call analysis with its summary switched on, and publish
|
|
235
|
+
refuses it otherwise. A text agent is always `transcript` and cannot be
|
|
236
|
+
changed: a chat thread has no boundary anyone would recognise.
|
|
237
|
+
- **`on_enter`, `on_exit`, `on_user_turn_completed`** — one such tool reference
|
|
238
|
+
each, run as lifecycle hooks.
|
|
239
|
+
|
|
240
|
+
## Draft and published
|
|
241
|
+
|
|
242
|
+
Every write lands on the **draft**. Live traffic runs the last **published**
|
|
243
|
+
version, which is an immutable snapshot. Nothing you change reaches a real
|
|
244
|
+
caller until the agent is published.
|
|
245
|
+
|
|
246
|
+
The order that matters:
|
|
247
|
+
|
|
248
|
+
1. Create or update the tool, including its operation tree.
|
|
249
|
+
2. `validate_tool`, then `publish_tool`. Attaching an unpublished tool is
|
|
250
|
+
rejected.
|
|
251
|
+
3. Attach it through the agent's `tools` or a hook.
|
|
252
|
+
4. `validate_agent`, then `publish_agent`.
|
|
253
|
+
|
|
254
|
+
Publishing an agent pins each attached tool to the tool version that is live *at
|
|
255
|
+
that moment*. Republishing a tool therefore does not change live behaviour until
|
|
256
|
+
the agent is published again — say so rather than leaving the user to discover
|
|
257
|
+
it.
|
|
258
|
+
|
|
259
|
+
## Writing config
|
|
260
|
+
|
|
261
|
+
`update_agent` and `create_agent` take the **whole** config, not a patch.
|
|
262
|
+
Anything you leave out reverts to its default — omitting `tools` detaches
|
|
263
|
+
every tool. Always read the current config first, apply your change to it, and
|
|
264
|
+
send the result back.
|
|
265
|
+
|
|
266
|
+
The config is validated on every write. Unknown models, a model that does not
|
|
267
|
+
support the channel, a language the model does not offer, a speed outside the
|
|
268
|
+
model's range, an unpublished tool, a knowledge base that is not ready — all are
|
|
269
|
+
refused with an explanation. Read the error and correct it; that is faster than
|
|
270
|
+
guessing.
|
|
271
|
+
|
|
272
|
+
Some fields are normalized for you. Switching to `text` clears `stt`, `tts`,
|
|
273
|
+
`greeting`, `turn_handling` and `avatar`. Switching to `video` fills in an
|
|
274
|
+
`avatar`, and always has `resume_false_interruption` off — the avatar cannot
|
|
275
|
+
resume a sentence it has already stopped rendering.
|
|
276
|
+
|
|
277
|
+
### Choosing models
|
|
278
|
+
|
|
279
|
+
`get_catalog` is the only source of valid `provider`/`model` pairs, and it also
|
|
280
|
+
states what each entry supports: which channels, its language codes and default
|
|
281
|
+
language, whether it accepts a speed and within what range, and its default
|
|
282
|
+
voice. Read it before choosing; do not assume a model exists.
|
|
283
|
+
|
|
284
|
+
### Images
|
|
285
|
+
|
|
286
|
+
People can attach a photo to a web chat, a web voice call or a web video call,
|
|
287
|
+
and the agent sees it. **There is no setting for this** — whether an agent can
|
|
288
|
+
read images is decided by the model it runs, and `vision` on that model's catalog
|
|
289
|
+
entry is where it says so. Every LLM entry today reads images; the two
|
|
290
|
+
`grok-voice-*` realtime entries do not, and an agent on one of them cannot be
|
|
291
|
+
sent a photo at all. If a user wants image input, check `vision` before picking
|
|
292
|
+
the model rather than changing anything on the agent.
|
|
293
|
+
|
|
294
|
+
A phone call carries no files, so this is web only.
|
|
295
|
+
|
|
296
|
+
### Vision input (screen share)
|
|
297
|
+
|
|
298
|
+
`vision_input` is what the agent *watches* during a call, as opposed to the
|
|
299
|
+
images people send it. Do not confuse it with `vision` on a catalog entry: that
|
|
300
|
+
is what the model *can* do, measured and not configurable, and it is the
|
|
301
|
+
precondition for this.
|
|
302
|
+
|
|
303
|
+
`vision_input.screenshare.enabled` lets the agent see the caller's screen while
|
|
304
|
+
they share it. On each of their turns the agent is handed the single newest
|
|
305
|
+
frame and only that one, so a long call does not grow slower or more expensive —
|
|
306
|
+
and when nobody is sharing, the agent is told so and asks rather than inventing a
|
|
307
|
+
screen. Set `vision_input.screenshare.record` as well to keep what was shared as a
|
|
308
|
+
1 fps video beside the call recording, deleted under the same retention policy.
|
|
309
|
+
|
|
310
|
+
Four rules decide whether it can be turned on at all:
|
|
311
|
+
|
|
312
|
+
- **Web voice and video calls only.** A phone call has no screen to share, and
|
|
313
|
+
the same agent config over a phone number simply never offers it — no setting
|
|
314
|
+
changes, and the agent is never told it can see.
|
|
315
|
+
- **Cascade only.** Turning it on with `realtime` set is refused when the agent
|
|
316
|
+
is saved: a speech-to-speech model detects turns inside the provider's socket,
|
|
317
|
+
so there is no moment at which it could be handed a frame.
|
|
318
|
+
- **The model must read images.** Publish fails, naming the model, if the LLM or
|
|
319
|
+
its fallback has `vision: false`. Check `get_catalog` before choosing.
|
|
320
|
+
- **The caller's client has to publish a track.** Our dashboard's test call and
|
|
321
|
+
the TypeScript SDK's `useTalqingScreenShare` both do; a customer's own surface
|
|
322
|
+
has to call `getDisplayMedia` from a click, which is a browser rule nothing on
|
|
323
|
+
our side can work around. So the agent asking out loud is what starts sharing.
|
|
324
|
+
|
|
325
|
+
It also forces `turn_handling.preemptive_generation` off, because the frame
|
|
326
|
+
changes the context the speculative reply was generated against.
|
|
327
|
+
|
|
328
|
+
### How long the model may think
|
|
329
|
+
|
|
330
|
+
**On a voice or video agent, give the model the least thinking it will accept.**
|
|
331
|
+
Every second of reasoning is a second of silence on a live call, and a caller who
|
|
332
|
+
hears nothing assumes the line has dropped. Take the **first** value in that
|
|
333
|
+
model's catalog `reasoning_efforts` list, or leave `reasoning_effort` unset —
|
|
334
|
+
the list is ordered fastest first, and unset already means that first value.
|
|
335
|
+
Raise it only when the user asks for it and accepts the pause, and tell them what
|
|
336
|
+
it costs them in silence.
|
|
337
|
+
|
|
338
|
+
Do not reason about the effort names, because they do not order the same way at
|
|
339
|
+
every vendor. `minimal` sounds like the floor and is not: on xAI's grok-4.3 it
|
|
340
|
+
burned *more* thinking than `low`, so `none` is the only setting there that
|
|
341
|
+
truly stops it. OpenAI rejects `minimal` outright and starts at `none`. The
|
|
342
|
+
catalog's order is measured; the names are marketing.
|
|
343
|
+
|
|
344
|
+
A text agent has no such pressure — nobody is listening to silence — so spend
|
|
345
|
+
thinking there freely when the task benefits from it.
|
|
346
|
+
|
|
347
|
+
Language is set **once**, on the agent: `config.language`. There is no language
|
|
348
|
+
field on `stt`, `tts` or `realtime`. Each model translates the agent's language
|
|
349
|
+
into its own spelling, so Hindi reaches Deepgram as `hi` and Sarvam as `hi-IN`
|
|
350
|
+
without you doing anything. Rules worth knowing before you hit them:
|
|
351
|
+
|
|
352
|
+
- Use a code that appears in some model's catalog languages; `null` means Auto.
|
|
353
|
+
- A model that publishes languages must be able to speak the one you chose, or
|
|
354
|
+
the write is refused. Prefer picking the language first, then models that
|
|
355
|
+
cover it.
|
|
356
|
+
- **A model with `language_required: true` refuses Auto**, because its API has no
|
|
357
|
+
detection to fall back on — Soniox TTS is like this, and an agent on it will not
|
|
358
|
+
publish until `config.language` is set. Every other model takes a null
|
|
359
|
+
`language`. On Soniox TTS the setting is not cosmetic: the code chosen is the
|
|
360
|
+
phonetic system the text is read through, so English sent as `es` comes out as
|
|
361
|
+
noise rather than accented English.
|
|
362
|
+
- **Auto is not detection on Raya.** None of its three entries detect anything;
|
|
363
|
+
they send their `default_language` (`en`) when the agent is on Auto. For a Raya
|
|
364
|
+
agent taking Indic calls, set `config.language` — Hindi audio transcribed as
|
|
365
|
+
English does not come back transliterated, it comes back with words missing, and
|
|
366
|
+
the agent never learns it misheard.
|
|
367
|
+
- **Auto does not mean "detects" everywhere.** A model with a `default_language`
|
|
368
|
+
runs on that code when the agent is on Auto, and the ones spelled `unknown`,
|
|
369
|
+
`multi` or `auto` are the providers' own words for detection. The rest are
|
|
370
|
+
real languages, and English is what a model that cannot detect falls back to:
|
|
371
|
+
`deepgram/nova-3` and both xAI speech-to-text models transcribe as English on
|
|
372
|
+
Auto and will mistranscribe a caller who speaks anything else. For an agent
|
|
373
|
+
taking non-English calls on Auto, pick `deepgram/nova-3-general` (`multi`),
|
|
374
|
+
`deepgram/flux-general-multi`, `sarvam/saaras:v3` (`unknown`), ElevenLabs
|
|
375
|
+
Scribe or OpenAI — all of which detect for real.
|
|
376
|
+
- Models that publish no languages (OpenAI TTS is multilingual, Deepgram encodes
|
|
377
|
+
the language in the voice) simply ignore the setting.
|
|
378
|
+
|
|
379
|
+
A catalog entry may carry a `note` — a caveat the *prompt* has to answer, not a
|
|
380
|
+
description of the model. Read it before writing the prompt and tell the user what
|
|
381
|
+
you did about it. No entry carries one today.
|
|
382
|
+
|
|
383
|
+
An STT entry with `streaming: false` transcribes each utterance in one request
|
|
384
|
+
instead of over a socket. That buys a lower price, and costs the round-trip:
|
|
385
|
+
the agent replies about half a second later than it would on a streaming model,
|
|
386
|
+
and word-count interruption stops working because there are no interim
|
|
387
|
+
transcripts. Prefer a streaming model unless price is the point.
|
|
388
|
+
|
|
389
|
+
`stt`, `llm` and `tts` each take an optional `fallback` — a second
|
|
390
|
+
provider/model the agent switches to mid-call if the primary starts failing.
|
|
391
|
+
Leave it unset unless the user asks for redundancy; it is not free to set up:
|
|
392
|
+
the fallback provider needs its own BYOK key before the agent will publish, and
|
|
393
|
+
a session that fails over is billed to both providers for the parts each one
|
|
394
|
+
served. The fallback must be a different provider/model from the primary, and it
|
|
395
|
+
cannot have a fallback of its own. A speech-to-text fallback must also match the
|
|
396
|
+
primary's `streaming` flag —
|
|
397
|
+
streaming backs streaming, batch backs batch — so end-of-turn timing stays the
|
|
398
|
+
same before and after a failover.
|
|
399
|
+
|
|
400
|
+
`llm.builtin_tools` — and `llm.fallback.builtin_tools`, separately — switches on the tools the model provider runs itself during
|
|
401
|
+
the reply — web search, X search, a code sandbox, a provider-side document
|
|
402
|
+
store. Each is `{type, config}`, and the *only* valid types and config keys are
|
|
403
|
+
the ones the chosen model's catalog entry lists under `builtin_tools`, so read
|
|
404
|
+
`catalog` before setting them. They are not Talqing tools: there is nothing to
|
|
405
|
+
create, publish or attach, and nothing runs on our side. Two things to tell the
|
|
406
|
+
user before switching one on — the provider bills per call, which our cost
|
|
407
|
+
estimate does not include; and the search or code run happens *before* the agent
|
|
408
|
+
speaks, which on a voice call is a few seconds of silence, so the prompt should
|
|
409
|
+
have the agent say it is looking something up. The failover model carries its
|
|
410
|
+
own list, set against its own catalog entry: the same tool name is a different
|
|
411
|
+
object at each vendor, so one model's settings are never copied to the other. A
|
|
412
|
+
failover with fewer tools (or none) is fine — it keeps the call and loses the
|
|
413
|
+
search, which is the right way round.
|
|
414
|
+
|
|
415
|
+
`catalog_voices` browses the voices for a provider — use it before picking a
|
|
416
|
+
voice by style, accent or gender. `catalog_avatars` is the face gallery for
|
|
417
|
+
video agents; an avatar's `id` becomes `avatar.avatar_id`, while `avatar.model`
|
|
418
|
+
comes from the provider catalog.
|
|
419
|
+
|
|
420
|
+
**Setting an ElevenLabs voice takes one more step.** After choosing the voice —
|
|
421
|
+
and, for a shared-library voice, after `add_elevenlabs_voice` has saved it — call
|
|
422
|
+
`elevenlabs_voice_settings` with the id you are going to use and copy the
|
|
423
|
+
`stability` and `similarity_boost` it returns onto the same `tts` object as the
|
|
424
|
+
voice. ElevenLabs applies a voice's own tuning only to a request that carries no
|
|
425
|
+
overrides at all, and Talqing always sends a speed, so a voice set without these
|
|
426
|
+
speaks in ElevenLabs' generic default instead of the voice its author shipped —
|
|
427
|
+
audibly, and with nothing to show why. They belong to that voice: replace them
|
|
428
|
+
whenever you change it, and clear them if you move to a model whose catalog entry
|
|
429
|
+
does not set `supports_voice_settings` (only ElevenLabs does), which agent
|
|
430
|
+
validation refuses outright.
|
|
431
|
+
|
|
432
|
+
### Expressive delivery
|
|
433
|
+
|
|
434
|
+
`tts.expressive` lets the agent write delivery tags — a laugh, a whisper, a
|
|
435
|
+
pause before the key detail — into the words it speaks. Only for a model whose
|
|
436
|
+
catalog entry carries an `expressive` block; that block's `prompt` is what gets
|
|
437
|
+
appended to the agent's system prompt, and the tag vocabulary is inside it. Read
|
|
438
|
+
it before writing a tag anywhere yourself: the dialects differ per model, and a
|
|
439
|
+
tag from the wrong one is silently dropped rather than spoken.
|
|
440
|
+
|
|
441
|
+
It costs a little on both sides of a turn — the added prompt, and every tag the
|
|
442
|
+
model emits as billed output. Say so before switching it on.
|
|
443
|
+
|
|
444
|
+
Two rules the API enforces, both at publish:
|
|
445
|
+
|
|
446
|
+
- A fallback voice must match the primary's `expressive` setting. The dialect is
|
|
447
|
+
taught once, from the primary, and a failover happens mid-turn — so the
|
|
448
|
+
fallback has to be a voice that speaks tags too. In practice that means the
|
|
449
|
+
two dialect models back each other, or there is no fallback.
|
|
450
|
+
- Every handoff target of an expressive agent must use the **same** provider,
|
|
451
|
+
model and `expressive` setting. The target inherits a transcript full of tags
|
|
452
|
+
and starts writing its own; a voice that cannot speak them reads them out.
|
|
453
|
+
Handing off *into* an expressive agent is fine.
|
|
454
|
+
|
|
455
|
+
**Nothing validates a tag you write by hand** into a greeting, a `say` line or a
|
|
456
|
+
handoff message. On a voice with no dialect it reaches the caller as the word
|
|
457
|
+
itself, so do not put one there unless the agent's own voice speaks it.
|
|
458
|
+
|
|
459
|
+
### Turn detection
|
|
460
|
+
|
|
461
|
+
**There is no setting for this.** What ends the caller's turn follows from the
|
|
462
|
+
speech-to-text model and `config.language`, so do not go looking for a knob and
|
|
463
|
+
do not promise the user one — change the models or the language instead:
|
|
464
|
+
|
|
465
|
+
- **Streaming speech-to-text** → its own end-of-speech detector ends the turn. It
|
|
466
|
+
can keep listening when it hears the caller is not finished, so
|
|
467
|
+
`endpointing.min_silence_duration` is the window it gets rather than the whole
|
|
468
|
+
wait, and its transcription round-trip lands on top. Deepgram Flux and Sarvam
|
|
469
|
+
Saaras endpoint on their own schedule and ignore that window entirely, so on
|
|
470
|
+
those two the setting is a floor.
|
|
471
|
+
- **Batch speech-to-text** (`streaming: false` — xAI `xai-stt-batch`, ElevenLabs
|
|
472
|
+
`scribe_v2`, OpenAI `gpt-4o-transcribe`) → it has no endpointer at all, so
|
|
473
|
+
LiveKit's audio end-of-turn model takes over when `config.language` is one of
|
|
474
|
+
the fourteen it was trained on: Arabic, Chinese, Dutch, English, French,
|
|
475
|
+
German, Hindi, Indonesian, Italian, Japanese, Korean, Portuguese, Spanish,
|
|
476
|
+
Turkish. It judges whether the *sentence* sounds finished, so it waits through
|
|
477
|
+
a caller who pauses mid-thought ("I need to think about that for… a moment").
|
|
478
|
+
Any other language, or Auto, falls back to plain silence.
|
|
479
|
+
|
|
480
|
+
So if the user complains the agent cuts people off mid-sentence, the lever is a
|
|
481
|
+
batch model plus one of those fourteen languages — that is the only pipeline that
|
|
482
|
+
hears meaning rather than silence. Otherwise raise
|
|
483
|
+
`endpointing.min_silence_duration` (minimum 0.25s).
|
|
484
|
+
|
|
485
|
+
`endpointing.max_silence_duration` is how long the agent waits when the end-of-turn
|
|
486
|
+
model says the caller is mid-thought, so it only does anything on that same
|
|
487
|
+
batch-plus-supported-language pipeline.
|
|
488
|
+
|
|
489
|
+
### Video
|
|
490
|
+
|
|
491
|
+
`channel: "video"` is a voice agent wearing an Anam avatar. The avatar joins the
|
|
492
|
+
call as a second participant and lip-syncs to the agent's own audio, whichever
|
|
493
|
+
model produced it — so video runs on either pipeline, the STT-LLM-TTS cascade or
|
|
494
|
+
a realtime speech-to-speech model, and everything true of that pipeline on
|
|
495
|
+
`voice` stays true here.
|
|
496
|
+
|
|
497
|
+
A video agent needs both an avatar model and an `avatar_id` before it will
|
|
498
|
+
publish; `get_catalog` lists the avatar models and `catalog_avatars` the faces.
|
|
499
|
+
Avatar time bills per wall-clock minute for the whole call, idle included, on top
|
|
500
|
+
of whatever the models cost.
|
|
501
|
+
|
|
502
|
+
## Tools
|
|
503
|
+
|
|
504
|
+
A tool is one capability the agent's LLM can call mid-conversation. It has:
|
|
505
|
+
|
|
506
|
+
- **`name`** — the function name the LLM sees. A valid identifier, snake_case,
|
|
507
|
+
describing the capability. `knowledge_fetch` is reserved.
|
|
508
|
+
- **`description`** — tells the LLM *when* to call it. This matters as much as
|
|
509
|
+
the implementation.
|
|
510
|
+
- **`json_schema`** — the arguments the LLM supplies. Declare only values the
|
|
511
|
+
agent can know or ask the caller for. **Give every property a `description`**:
|
|
512
|
+
it is the only instruction the agent's model gets on what to put there and how
|
|
513
|
+
to get it out of the conversation, and a bare `{"type": "string"}` is how a
|
|
514
|
+
tool ends up called with the wrong value. Mark the ones the tool cannot run
|
|
515
|
+
without as `required`, and use an `enum` wherever the set of values is closed.
|
|
516
|
+
- **`long_running_task`** — voice and video agents keep talking while it runs;
|
|
517
|
+
text agents wait for the result either way.
|
|
518
|
+
- **`silent`** — no immediate reply after it runs. You rarely need to set it: a
|
|
519
|
+
tool in which no operation can return a response (see below) is silent
|
|
520
|
+
automatically, and so is any run that reaches `end_call`.
|
|
521
|
+
- **`disable_interruptions`** — the caller cannot barge in while it works.
|
|
522
|
+
- **`operations`** — the tree that runs when it is called.
|
|
523
|
+
|
|
524
|
+
Prefer one meaningful business capability per tool over many thin ones.
|
|
525
|
+
|
|
526
|
+
### The operation tree
|
|
527
|
+
|
|
528
|
+
`operations` is an ordered list. Sending it replaces the tree entirely, so
|
|
529
|
+
include every node you want to keep.
|
|
530
|
+
|
|
531
|
+
Each node is `{kind, config, on_error?}`, and `kind` decides the rest: an
|
|
532
|
+
operation is a union of eleven shapes, one per kind, each carrying only the
|
|
533
|
+
fields that kind has. `on_error` is `abort` (default) or `continue`.
|
|
534
|
+
|
|
535
|
+
`silent`, `publish_fields` and `background_execution` exist on `http`, `code` and
|
|
536
|
+
`frontend_rpc` and on nothing else, because those three are the only kinds that
|
|
537
|
+
produce a result — there is nothing for the other eight to hide, to publish out
|
|
538
|
+
of, or to stop waiting for. `then` and `else` belong to an `if` in the same way.
|
|
539
|
+
Sending one of them on a kind that does not have it is rejected, not ignored.
|
|
540
|
+
|
|
541
|
+
`silent: true` still runs the operation but hides its response from the agent's
|
|
542
|
+
LLM — use it for noisy intermediate steps.
|
|
543
|
+
|
|
544
|
+
**Silence is derived, not defaulted.** If no operation in the tree can return a
|
|
545
|
+
response, the tool is silent whatever `silent` says — including operations inside
|
|
546
|
+
`if` branches. So a tool that is just `say "Your appointment is confirmed"` says
|
|
547
|
+
that once, rather than saying it and then improvising a second sentence on top.
|
|
548
|
+
If the agent should add a closing line, add a `generate_reply` operation that
|
|
549
|
+
states what to add; do not try to turn the derived silence off.
|
|
550
|
+
|
|
551
|
+
**Templates.** `{{args.field}}` reads the tool's arguments, `{{tooldata.field}}`
|
|
552
|
+
this tool run's own state, `{{userdata.field}}` the session's state,
|
|
553
|
+
`{{system_vars.field}}` what the platform knows about this session,
|
|
554
|
+
`{{vars.field}}` what the tenant supplied for it, and `{{secrets.NAME}}` a
|
|
555
|
+
workspace secret. A string that is exactly one template token keeps the resolved
|
|
556
|
+
value's type. Every one of these works in a URL, in the query, in the body **and
|
|
557
|
+
in headers**. A token whose root is not one of those six is an error at save,
|
|
558
|
+
not text.
|
|
559
|
+
|
|
560
|
+
`system_vars` is read-only, and its six keys are the same ones the agent's prompt
|
|
561
|
+
reads: `human_phone_number`, `agent_phone_number`, `direction`, `now`, `date`,
|
|
562
|
+
`time`. So a tool can POST the caller's number to a CRM, stamp a record with
|
|
563
|
+
`{{system_vars.now}}`, or branch an `if` on the direction without the agent
|
|
564
|
+
having to ask. The phone-call keys are empty on web calls, on text conversations
|
|
565
|
+
and in a dashboard test run; the clock keys resolve wherever the agent has a
|
|
566
|
+
`timezone`, and read the time the **tool ran** rather than the time the call
|
|
567
|
+
started.
|
|
568
|
+
|
|
569
|
+
`vars` is read-only too: whatever the calling agent declares, with whatever the
|
|
570
|
+
request that started the session supplied merged over it — so
|
|
571
|
+
`https://{{vars.api_domain}}/book` is one tool serving every reseller, without a
|
|
572
|
+
copy of it per customer. A tool is workspace-level and does not know which agent
|
|
573
|
+
will call it, so a `{{vars.…}}` it reads is only checked against declarations
|
|
574
|
+
when an agent that attaches it is published.
|
|
575
|
+
|
|
576
|
+
Do not compare `{{system_vars.time}}` or `{{system_vars.date}}` in an `if`: `gt`
|
|
577
|
+
and `lt` need two numbers, so `"7:26 PM"` against `"09:00"` fails the operation
|
|
578
|
+
rather than quietly taking `else`. Business-hours logic belongs in a `code`
|
|
579
|
+
operation over `new Date(input.system_vars.now)`.
|
|
580
|
+
|
|
581
|
+
**The two stores.** `tooldata` is created empty every time the tool runs and is
|
|
582
|
+
gone when it returns — only later operations in the same tree can read it.
|
|
583
|
+
`userdata` belongs to the session: other tools, later turns and the agent's
|
|
584
|
+
prompt read it, and it is persisted with the call. Prefer `tooldata` for the
|
|
585
|
+
plumbing between two operations, and `userdata` only for what the conversation
|
|
586
|
+
should still know afterwards.
|
|
587
|
+
|
|
588
|
+
**Dataflow.** An operation's result is *not* automatically visible to the next
|
|
589
|
+
one. `http`, `code` and `frontend_rpc` may declare `publish_fields`, each
|
|
590
|
+
`{path, key?, store}`: `path` picks a value out of the response, `key` names it
|
|
591
|
+
(the last path segment when omitted), and `store` is `tooldata` or `userdata`.
|
|
592
|
+
`store` is required — it is the difference between a value that dies with the
|
|
593
|
+
tool and one the whole session carries.
|
|
594
|
+
|
|
595
|
+
`set_variable` writes one templated value directly, with the same required
|
|
596
|
+
`store`.
|
|
597
|
+
|
|
598
|
+
Reading `{{tooldata.X}}` when nothing earlier in the tool publishes `X` is an
|
|
599
|
+
**error** — tooldata starts empty, so that read can only resolve to nothing.
|
|
600
|
+
Reading `{{userdata.X}}` that the tool does not publish is only a **warning**:
|
|
601
|
+
X has to already be in session userdata, which means something else — the API
|
|
602
|
+
caller, an earlier tool — must put it there first. Say that in the agent's prompt.
|
|
603
|
+
|
|
604
|
+
**Kinds:**
|
|
605
|
+
|
|
606
|
+
Each kind's fields, their bounds and their defaults are in the schema; what
|
|
607
|
+
follows is what the schema cannot tell you. Every `timeout` is in **seconds**,
|
|
608
|
+
never milliseconds: the caller is on the line while an operation runs, so a long
|
|
609
|
+
timeout is a hung call rather than a patient one — reach for `long_running_task`
|
|
610
|
+
instead.
|
|
611
|
+
|
|
612
|
+
- **`http`** — calls a REST API. Expects JSON; a non-2xx status or a non-JSON
|
|
613
|
+
response fails the operation. Every root templates into headers as well as the
|
|
614
|
+
URL, query and body — but an `Authorization` built out of `{{args.…}}` is a
|
|
615
|
+
credential the *model* chose, so put credentials in `{{secrets.…}}`.
|
|
616
|
+
- **`code`** — TypeScript for transformations and shaping. `source_ts` must
|
|
617
|
+
`export default async function handler(input)`, where `input` is
|
|
618
|
+
`{args, tooldata, userdata, system_vars, vars, secrets}`, and return an object.
|
|
619
|
+
The sandbox has `console` and a guarded `fetch` for public http(s) URLs — no
|
|
620
|
+
Node APIs, no imports.
|
|
621
|
+
|
|
622
|
+
That input object is the whole interface: a code operation has **no template
|
|
623
|
+
syntax**. Write `input.system_vars.now`, never `{{system_vars.now}}` — braces
|
|
624
|
+
in the TypeScript are neither substituted nor flagged, so a `{{args.id}}`
|
|
625
|
+
inside a string literal ships to the tenant's API as literal braces. (This is
|
|
626
|
+
also the only place braces are allowed to survive, so a script that needs to
|
|
627
|
+
build a mustache template for something downstream can.)
|
|
628
|
+
- **`if`** — compares `left` with `right` and runs its `then` or its `else`.
|
|
629
|
+
Because an `if` is terminal in its chain, `on_error: "continue"` on one means
|
|
630
|
+
*neither branch runs and the tree ends there* — a real outcome to reach for,
|
|
631
|
+
and the only thing `continue` can mean on an operation nothing follows.
|
|
632
|
+
- **`set_variable`** — writes one templated value into `tooldata` or `userdata`.
|
|
633
|
+
- **`say`** — spoken verbatim.
|
|
634
|
+
- **`generate_reply`** — the LLM writes the reply.
|
|
635
|
+
- **`add_message`** — adds a system message without triggering a reply.
|
|
636
|
+
- **`end_call`** — put a `say` immediately before it if there should be a
|
|
637
|
+
goodbye, and word that goodbye as what actually happens: the call ends here.
|
|
638
|
+
Never say "let me transfer you", "please hold" or "someone will call you
|
|
639
|
+
back" before an `end_call` — the caller hears a promise and then silence. The
|
|
640
|
+
goodbye always finishes playing before the call drops, and the agent never
|
|
641
|
+
says anything after it, so `wait_for_playback` on that `say` is unnecessary.
|
|
642
|
+
- **`handoff`** — moves the call to another agent. `context` is `transcript`,
|
|
643
|
+
`summary` or `none` — the same three words as an agent's own
|
|
644
|
+
`conversation.context`, because it is the same question about the next agent
|
|
645
|
+
rather than the next call. A named
|
|
646
|
+
`target_agent_id` must already be published; `agent_name` resolves against the
|
|
647
|
+
team defined on the call. Handoff moves the call between **agents on this
|
|
648
|
+
platform**. To reach a *person*, use `transfer`.
|
|
649
|
+
|
|
650
|
+
`context: "summary"` **requires** a `summary`, and it is a value rather than a
|
|
651
|
+
request: this tree runs after the model's tool call, so there is no argument
|
|
652
|
+
being written at that moment to take one from. Point it at where the text comes
|
|
653
|
+
from — `{{args.summary}}` (a property on this tool's own `json_schema` that the
|
|
654
|
+
model filled), `{{tooldata.brief}}` (something an earlier `http` or `code` op
|
|
655
|
+
published), or fixed prose. There is no second LLM call either way. A `summary`
|
|
656
|
+
under any other context is an error.
|
|
657
|
+
|
|
658
|
+
`recent_turns` works exactly as it does on `handoffs[]`: the tail that
|
|
659
|
+
crosses verbatim, defaulting to two turns under `summary` and to none under
|
|
660
|
+
`none`, and rejected under `transcript`.
|
|
661
|
+
|
|
662
|
+
Reach for this operation only when the decision is the TREE's — after an
|
|
663
|
+
`http` lookup, inside an `if`. When the model should decide which desk the
|
|
664
|
+
caller wants, put the destinations on the agent's own `handoffs` field
|
|
665
|
+
instead: that is one line per edge rather than a tool with a lifecycle.
|
|
666
|
+
- **`transfer`** — hands the caller to a human being on `destination`, a literal
|
|
667
|
+
phone number in full international format (`+14155550101`). **Phone calls
|
|
668
|
+
only** — on a web or text session the operation fails.
|
|
669
|
+
|
|
670
|
+
The number is fixed at publish and is never templated or chosen by the model:
|
|
671
|
+
`{{args.number}}` is not accepted, and neither is a number the caller reads
|
|
672
|
+
out. If a user asks for either, say so plainly — it is a toll-fraud rule, not
|
|
673
|
+
an oversight.
|
|
674
|
+
|
|
675
|
+
How the caller is reached is the platform's business, not the builder's: we
|
|
676
|
+
dial the person into the call and step out. Do not offer a choice, and do not
|
|
677
|
+
mention SIP. One consequence is worth stating if a user asks about it: the
|
|
678
|
+
person answering sees the workspace's own number, not the caller's, because
|
|
679
|
+
the leg is dialled from the workspace's trunk.
|
|
680
|
+
|
|
681
|
+
`mode` decides what the *caller* experiences, and it is the only choice the
|
|
682
|
+
builder gets about how a transfer runs:
|
|
683
|
+
|
|
684
|
+
- **`cold`** (the default) — the agent finishes its line, then the caller
|
|
685
|
+
hears hold music for as long as the other phone takes to ring, and a person
|
|
686
|
+
answers. There is no introduction and no "let me hand you over to Sarah".
|
|
687
|
+
- **`warm`** — the caller goes on hold with hold music while the agent rings
|
|
688
|
+
the person on a separate line, tells them who is calling and what they want,
|
|
689
|
+
answers their questions, and only then puts the two together. The caller
|
|
690
|
+
hears none of the briefing. It costs a second AI conversation, which is
|
|
691
|
+
billed to the workspace like any other, and it takes as long as the briefing
|
|
692
|
+
takes — so it suits escalation to a colleague, not a busy queue.
|
|
693
|
+
|
|
694
|
+
Pick `warm` when the person answering needs context to be useful, or when the
|
|
695
|
+
user says "brief them first" / "don't just dump the caller on them". Pick
|
|
696
|
+
`cold` for a straight "put me through to sales". If the user has not said,
|
|
697
|
+
ask — the difference is a minute of hold music, and it is theirs to choose.
|
|
698
|
+
|
|
699
|
+
With `warm` the person answering can also **decline**, and then the caller
|
|
700
|
+
comes back to the agent and the call carries on. The agent is told why in
|
|
701
|
+
general terms; it never hears or repeats that person's own words.
|
|
702
|
+
|
|
703
|
+
`ringing_timeout` bounds the *ringing*, not a warm briefing. `on_failure` is
|
|
704
|
+
`continue` (the default: the agent is told
|
|
705
|
+
plainly why and keeps talking to the caller) or `end_call`. What the agent is
|
|
706
|
+
told is always plain English about the person being called — "the line was
|
|
707
|
+
busy", "nobody answered", "they aren't able to take the call right now", "the
|
|
708
|
+
call went through to their voicemail" — never a technical reason.
|
|
709
|
+
|
|
710
|
+
Put a `say` immediately **before** it if the caller should hear a line, and
|
|
711
|
+
don't bother with `wait_for_playback` — the transfer already waits for it to
|
|
712
|
+
finish. Unlike `end_call`, "let me transfer you" is a promise the platform can
|
|
713
|
+
keep, so say it here and never before an `end_call`. Word it for the mode:
|
|
714
|
+
before a `warm` transfer, "let me speak to them first and bring you in" is
|
|
715
|
+
true; before a `cold` one it is a lie.
|
|
716
|
+
|
|
717
|
+
Once the transfer succeeds this call is over as far as the platform is
|
|
718
|
+
concerned — the agent says nothing more, and the call's duration and recording
|
|
719
|
+
cover only the part the agent was on. On `warm`, the briefing itself is not
|
|
720
|
+
recorded either; it is kept on the call's timeline as a `transfer.briefing`
|
|
721
|
+
event so a workspace can review what was said about a caller.
|
|
722
|
+
|
|
723
|
+
**Multiple destinations: one tool per destination, not one tool that
|
|
724
|
+
branches.** `transfer_to_billing`, `transfer_to_sales`, `transfer_to_support`
|
|
725
|
+
— each argument-free, each one `say` and one `transfer`. The model then picks
|
|
726
|
+
a *tool*, which is the thing it chooses most reliably, every destination gets
|
|
727
|
+
its own name and description, and the tool list reads like the org chart.
|
|
728
|
+
Nesting five `if`s inside one tool is worse in every one of those respects.
|
|
729
|
+
|
|
730
|
+
**The tool's `description` is the escalation policy.** For every other kind a
|
|
731
|
+
weak description costs a mis-timed API call; here it decides whether a
|
|
732
|
+
frustrated caller reaches a person. "Transfer to a human" invites the model to
|
|
733
|
+
escalate at the first sign of difficulty. "Use only after you have tried to
|
|
734
|
+
answer the question and the caller has asked for a person, or is clearly
|
|
735
|
+
distressed" is a policy. Ask the user *when* escalation should happen rather
|
|
736
|
+
than assuming, and write the second kind.
|
|
737
|
+
|
|
738
|
+
Tick tool-level `disable_interruptions` on a transfer tool, so the caller
|
|
739
|
+
cannot talk over "connecting you now" and turn a waited line into an
|
|
740
|
+
interrupted one. Two shapes it cannot have: the tool may not be
|
|
741
|
+
`long_running_task`, and it may not be a lifecycle hook — both are refused at
|
|
742
|
+
publish, because the handover has to happen inside the turn that asked for it.
|
|
743
|
+
- **`frontend_rpc`** — awaits a handler on the connected web client and can
|
|
744
|
+
publish from its response.
|
|
745
|
+
|
|
746
|
+
`background_execution: true` on `http`, `code` or `frontend_rpc` fires and
|
|
747
|
+
forgets — no response, so no `publish_fields`.
|
|
748
|
+
|
|
749
|
+
**`wait_for_playback`** (on `say` and `generate_reply`) holds the
|
|
750
|
+
tree until that line has finished playing. Leave it off — the usual `say` is a
|
|
751
|
+
filler that exists to *cover* the next operation's latency ("let me pull that up
|
|
752
|
+
for you" → `http` → "your balance is …"), and waiting there lengthens every call
|
|
753
|
+
for nothing. Tick it only when the caller must have heard the line before the
|
|
754
|
+
next operation runs: a disclosure before a recording starts, or a promise before
|
|
755
|
+
a side effect they might still retract. Two things to know before relying on it:
|
|
756
|
+
the wait also covers whatever the agent was already saying in the same turn (so
|
|
757
|
+
on a filler it can stall the call for several seconds), and an interrupted line
|
|
758
|
+
counts as finished — it is not proof the caller heard it. Pair it with tool-level
|
|
759
|
+
`disable_interruptions` when that matters.
|
|
760
|
+
|
|
761
|
+
**The branching rule.** `if`, `handoff`, `end_call` and `transfer` are terminal
|
|
762
|
+
in their chain, and a chain is the top-level list or any `then`/`else` list.
|
|
763
|
+
Nothing may follow them there, and branches do not rejoin. If work must happen after a branch,
|
|
764
|
+
duplicate it into both branches or move the branch later. For an early-exit guard,
|
|
765
|
+
put the rest of the flow inside the guard's `else`. Anything the tool has to do
|
|
766
|
+
before hanging up — logging the outcome, posting a lead — goes **before** the
|
|
767
|
+
`end_call`, not after it: the session is already closing by then.
|
|
768
|
+
|
|
769
|
+
## Lifecycle hooks
|
|
770
|
+
|
|
771
|
+
Each hook points at a **published tool id**, or null.
|
|
772
|
+
|
|
773
|
+
- **`on_enter`** — a session opens. On voice and video it runs alongside the
|
|
774
|
+
greeting on first entry; on text, when the conversation window opens. Handoffs
|
|
775
|
+
skip the greeting.
|
|
776
|
+
- **`on_exit`** — the session closes: call end or handoff on voice and video;
|
|
777
|
+
on text, `end_call`, a closed window, or about a minute idle.
|
|
778
|
+
- **`on_user_turn_completed`** — after every user turn, before the LLM replies.
|
|
779
|
+
Each spoken turn on voice and video, each inbound message on text. The user's
|
|
780
|
+
words arrive as `{{args.user_message}}`. On an agent with
|
|
781
|
+
`vision_input.screenshare` on, the newest frame of the shared screen is already
|
|
782
|
+
in the context this hook runs against, so anything the tool adds lands after
|
|
783
|
+
it.
|
|
784
|
+
|
|
785
|
+
## Agent tasks
|
|
786
|
+
|
|
787
|
+
An **agent task** is an agent nobody talks to. Same prompt, same model, same
|
|
788
|
+
tools, same MCP servers — minus every conversational organ (speech, greeting,
|
|
789
|
+
turn-taking, handoffs, knowledge bases), plus the one thing a conversation
|
|
790
|
+
cannot have: a **typed structured output**. It takes named inputs, does some
|
|
791
|
+
work, and returns a value. `POST /v1/tasks/{task_id}/runs` runs one and waits.
|
|
792
|
+
|
|
793
|
+
It is its own noun with its own operations (`list_tasks`, `create_task`,
|
|
794
|
+
`get_task`, `update_task`, `delete_task`, `run_task`, `list_task_runs`) — not a
|
|
795
|
+
fourth agent channel. Use one wherever the work has an input and an answer
|
|
796
|
+
rather than a conversation: research a company from a domain, classify a
|
|
797
|
+
message, draft an opening line, pull a field out of a document.
|
|
798
|
+
|
|
799
|
+
`config` is:
|
|
800
|
+
|
|
801
|
+
- **`name`** — unique in the workspace.
|
|
802
|
+
- **`prompt`** — the system prompt, and the whole of the task's behaviour. It
|
|
803
|
+
reads `{{vars.name}}` for the values the run was given, and
|
|
804
|
+
`{{system_vars.now}}` / `.date` / `.time` when `timezone` is set. It does
|
|
805
|
+
**not** read `{{system_vars.human_phone_number}}` / `.agent_phone_number` /
|
|
806
|
+
`.direction` — there is no call.
|
|
807
|
+
- **`llm`** — one model, with the same `fallback`, `reasoning_effort`,
|
|
808
|
+
`priority` and `builtin_tools` an agent's LLM has. There is no `stt`, `tts`,
|
|
809
|
+
`realtime`, `greeting`, `turn_handling`, `avatar`, `handoffs`, `recording`,
|
|
810
|
+
`analysis`, `kb_ids` or lifecycle hook — a task has nobody to hear, nobody to
|
|
811
|
+
greet, nothing to hand over and one turn to hook.
|
|
812
|
+
- **`tools`** / **`mcps`** — exactly an agent's, attached by id or defined
|
|
813
|
+
inline. **A task never pins a tool version**: republishing a tool changes
|
|
814
|
+
every task that uses it, immediately, with no step in between. That is
|
|
815
|
+
defensible for a bounded job and would not be for a live call, which is why
|
|
816
|
+
agents pin and tasks do not.
|
|
817
|
+
- **`vars`** — the task's **inputs**, declared exactly as an agent's variables
|
|
818
|
+
are: a `name`, a `description`, an optional `default` and `required`. A run
|
|
819
|
+
supplies values by name; one it omits falls back to the `default`, and a
|
|
820
|
+
`required` variable with neither is refused *before* anything is compiled, so
|
|
821
|
+
that failure costs nothing. A value for a name the task does not declare is
|
|
822
|
+
refused too, rather than silently dropped — which is the one difference from an
|
|
823
|
+
agent, whose key space is open.
|
|
824
|
+
- **`output`** — a flat list of the fields the model must produce, each with a
|
|
825
|
+
`name`, a `type` (`string`, `boolean`, `integer` or `number`) and a
|
|
826
|
+
`description`. At least one, at most 25. No arrays and no nested objects: a
|
|
827
|
+
task that wants to return five talking points returns one string containing
|
|
828
|
+
them.
|
|
829
|
+
- **`max_steps`** (default 25, max 50) — how many LLM → tools → LLM **rounds**
|
|
830
|
+
the run may take. A round, not a tool call: four tools in one reply cost one
|
|
831
|
+
step. This is the runaway-loop guard.
|
|
832
|
+
- **`timeout_seconds`** (default 120, max 600) — the real time budget.
|
|
833
|
+
|
|
834
|
+
A name may not appear in both `vars` and `output`: a run's inputs and its output
|
|
835
|
+
are read side by side, so a collision would make one of the two unreachable.
|
|
836
|
+
|
|
837
|
+
**There is no draft and no publish step.** One config, strict at save: an
|
|
838
|
+
invalid one is refused with a 400 listing every problem and nothing is stored,
|
|
839
|
+
so a task that exists can always run. An edit takes effect on the next run with
|
|
840
|
+
nothing in between — there are no versions, so there is no earlier wording to go
|
|
841
|
+
back to and nothing records which definition produced an earlier run.
|
|
842
|
+
`create_task` and `update_task` also return **`warnings`**: the problems that
|
|
843
|
+
are real but not fatal, such as a prompt reading a variable nothing declares.
|
|
844
|
+
There is no publish screen to show them on, so read them where they are.
|
|
845
|
+
|
|
846
|
+
### The output tool
|
|
847
|
+
|
|
848
|
+
The model does not "return" the output. At compile time the task gains one
|
|
849
|
+
generated tool, **`submit_result`**, whose arguments are exactly the `output`
|
|
850
|
+
fields, and one generated paragraph telling it that calling that tool is how the
|
|
851
|
+
run finishes. Both are written for you: never declare a tool named
|
|
852
|
+
`submit_result`, and never write that paragraph into the prompt yourself.
|
|
853
|
+
|
|
854
|
+
Every output field is **required and nullable** in that tool. The model must say
|
|
855
|
+
something about every field, including "I could not find it" — an optional field
|
|
856
|
+
lets it silently omit the address it failed to find, and "omitted" and "not
|
|
857
|
+
generated yet" then look identical to whoever reads the row. Say so in each
|
|
858
|
+
field's `description`: that description is the only instruction the model gets
|
|
859
|
+
about what belongs there.
|
|
860
|
+
|
|
861
|
+
### Reading a run
|
|
862
|
+
|
|
863
|
+
`run_task` **executes for real**: the tools call the tenant's endpoints with
|
|
864
|
+
their secrets, the MCP servers spend their credits, and the model spends their
|
|
865
|
+
tokens. A task that books, charges or sends will do so.
|
|
866
|
+
|
|
867
|
+
The response is the whole run — `output`, a `trace` of every tool call and
|
|
868
|
+
everything the model wrote (secrets redacted, long fields truncated and marked),
|
|
869
|
+
`steps_used` against `max_steps`, the tokens each model spent and
|
|
870
|
+
`provider_cost`.
|
|
871
|
+
|
|
872
|
+
**`attempts` is worth reading.** A run that ends without calling `submit_result`
|
|
873
|
+
is re-prompted rather than failed, and **each re-prompt starts the step budget
|
|
874
|
+
over** — so `attempts: 3` means the run hit `max_steps` twice and recovered on
|
|
875
|
+
the third go, having used up to three times the rounds `steps_used` shows.
|
|
876
|
+
`steps_used` is the busiest attempt's count, because that is the number the cap
|
|
877
|
+
governs; `timeout_seconds`, not `max_steps`, is what actually bounds a run.
|
|
878
|
+
|
|
879
|
+
A failure carries `error.type`, and it is a closed vocabulary that says whose
|
|
880
|
+
problem it is:
|
|
881
|
+
|
|
882
|
+
- `missing_vars` — a required input had no value and no default. Caught before
|
|
883
|
+
anything ran, so it cost nothing.
|
|
884
|
+
- `no_output` — the model finished without calling `submit_result`, twice; the
|
|
885
|
+
framework already re-prompted it once. **Fix the prompt**: say what the task
|
|
886
|
+
is for and that finishing means calling the tool.
|
|
887
|
+
- `step_limit` — every round of an attempt was used and no result came back.
|
|
888
|
+
**Fix `max_steps`**, or narrow what the task does. Reported apart from
|
|
889
|
+
`no_output` precisely so nobody rewrites a prompt that was never the problem.
|
|
890
|
+
- `timeout` — `timeout_seconds` elapsed.
|
|
891
|
+
- `provider_error` — the model or an MCP server failed. Not yours to fix.
|
|
892
|
+
- `configuration` — yours to tell them: a missing BYOK key, a deleted or
|
|
893
|
+
unpublished tool, an integration whose credential no longer resolves.
|
|
894
|
+
- `platform` — ours.
|
|
895
|
+
|
|
896
|
+
Task runs are **not** in the Observability charts, which are built on calls and
|
|
897
|
+
conversations. `list_task_runs` is where a task's own history lives.
|
|
898
|
+
|
|
899
|
+
## Integrations
|
|
900
|
+
|
|
901
|
+
Integrations are external services, and they come in two shapes.
|
|
902
|
+
|
|
903
|
+
**MCP integrations** give an agent tools from an external server. Attach them
|
|
904
|
+
in the agent's own config, as `mcps` — the same place as `tools` and
|
|
905
|
+
`kb_ids`, written by `create_agent` or `update_agent`, and frozen at publish
|
|
906
|
+
like the rest of it. So attaching one **takes a publish** before a call sees it.
|
|
907
|
+
`list_integration_mcp_tools` connects to the server and lists everything it
|
|
908
|
+
offers, which also proves the connection works.
|
|
909
|
+
|
|
910
|
+
Which of those tools an agent may actually call is the integration's
|
|
911
|
+
`allowed_tools`, approved once on the integration and shared by every agent
|
|
912
|
+
attached to it. Null means all of them. Patch the list to narrow it — worth
|
|
913
|
+
doing on a large server, since every approved tool is described to the model on
|
|
914
|
+
every turn, and a voice agent picking from ninety of them is slower and less
|
|
915
|
+
accurate than one picking from six. Unlike the attachment, that approval is
|
|
916
|
+
live: narrowing it reaches every already-published agent on its next turn.
|
|
917
|
+
|
|
918
|
+
**Channel integrations** (Telegram) deploy an agent to a messaging
|
|
919
|
+
channel through *triggers*, not MCP attachment. A trigger binds an inbound event
|
|
920
|
+
to a published text agent and decides what happens with the reply.
|
|
921
|
+
|
|
922
|
+
`integration_catalog` is the authoritative list of providers and, in
|
|
923
|
+
`setup_fields`, exactly what each one needs — read it instead of guessing.
|
|
924
|
+
Providers whose `auth_type` is `oauth` must be connected from the dashboard's
|
|
925
|
+
Integrations page; you cannot create them through the API. Manual providers take
|
|
926
|
+
their credentials on create: pass the plaintext token or key and the platform
|
|
927
|
+
stores it as a workspace secret and wires the reference, or pass an existing
|
|
928
|
+
`{{secrets.NAME}}`.
|
|
929
|
+
|
|
930
|
+
## Phone numbers
|
|
931
|
+
|
|
932
|
+
A voice or video agent answers the phone once a number is pointed at it. Three
|
|
933
|
+
things have to line up, in order:
|
|
934
|
+
|
|
935
|
+
1. **A carrier account** — the credential for a phone network.
|
|
936
|
+
`list_telephony_providers` states exactly which fields each carrier needs;
|
|
937
|
+
`create_telephony_account` records them, and `provision_telephony_account`
|
|
938
|
+
builds the SIP trunk. Credentials may be passed as plaintext (stored as a
|
|
939
|
+
workspace secret) or as an existing `{{secrets.NAME}}`.
|
|
940
|
+
2. **A number on it** — `list_remote_numbers` shows what the account owns at
|
|
941
|
+
the carrier, and `import_phone_numbers` brings the chosen ones in and
|
|
942
|
+
provisions them. Numbers are bought at the carrier, never here. Import
|
|
943
|
+
reports per number: read every `ok` rather than trusting the 200.
|
|
944
|
+
3. **An agent on the number** — `assign_phone_number` puts a **published**
|
|
945
|
+
voice or video agent on it. Republishing the agent updates live calls
|
|
946
|
+
automatically; the assignment is not redone.
|
|
947
|
+
|
|
948
|
+
`readiness` on a number is the honest answer to "does this work": `live`,
|
|
949
|
+
`needs_agent`, `needs_carrier_setup`, `setting_up`, `error` or `disabled`.
|
|
950
|
+
|
|
951
|
+
Some carrier work happens in the carrier's own console and no API can confirm
|
|
952
|
+
it — that is what an account's `setup_steps` are for. A step marked
|
|
953
|
+
`user_confirmable` is done by the user, who then confirms it with
|
|
954
|
+
`patch_telephony_account`'s `console_setup_confirmed`. Do not claim inbound
|
|
955
|
+
calling works while such a step is outstanding.
|
|
956
|
+
|
|
957
|
+
## Batch outbound calling
|
|
958
|
+
|
|
959
|
+
To call a *list* of people rather than one, create a **batch**: an agent, one of
|
|
960
|
+
its outbound-capable numbers, a list of recipients, and a schedule.
|
|
961
|
+
`create_call_batch` is the only way to schedule a call for later — a single
|
|
962
|
+
scheduled call is a one-row batch — and it is the most expensive operation on
|
|
963
|
+
this platform, because one call to it can place thousands of billable dials.
|
|
964
|
+
|
|
965
|
+
**A batch is policy plus a recipient list.** The policy — schedule, business
|
|
966
|
+
hours, concurrency, agent, retry rule — can be changed at any time with
|
|
967
|
+
`patch_call_batch`, and the next dial picks it up. The recipient list is the
|
|
968
|
+
work: who has been called and how it went, one row each, read back through
|
|
969
|
+
`list_call_batch_recipients`.
|
|
970
|
+
|
|
971
|
+
**Personalization is `userdata`.** Each recipient carries `to` plus a string map
|
|
972
|
+
that becomes that call's session state, so `{{userdata.first_name}}` in the
|
|
973
|
+
agent's prompt or greeting is that person's name. There are no per-recipient
|
|
974
|
+
overrides of anything else — one batch is one agent, one number, one clock.
|
|
975
|
+
|
|
976
|
+
**Scheduling.** `timezone` is the batch's clock. `start_at` says when it begins
|
|
977
|
+
(omit it to start immediately) and `calling_window` says which hours and
|
|
978
|
+
weekdays it may dial in, so a list uploaded at 23:00 can start at 11:00
|
|
979
|
+
tomorrow, stop at 18:00 and resume at 10:00 on the next weekday untouched. A
|
|
980
|
+
window may cross midnight; the days then name the evening it starts on.
|
|
981
|
+
`next_dial_at` on the batch says when it will dial next, which is how you tell
|
|
982
|
+
"waiting for Monday" apart from "stuck".
|
|
983
|
+
|
|
984
|
+
**Say these two things before creating one**, because a user who discovers
|
|
985
|
+
either from an invoice will not forgive us:
|
|
986
|
+
|
|
987
|
+
- **Answering machines are not detected.** A call that reaches voicemail is
|
|
988
|
+
answered, billed, and reported as completed.
|
|
989
|
+
- **Every call runs the agent's current published version**, so republishing
|
|
990
|
+
mid-batch changes every call placed after that moment.
|
|
991
|
+
|
|
992
|
+
**Steering it.** `pause_call_batch` stops new calls and lets live ones finish;
|
|
993
|
+
`resume_call_batch` carries on. `cancel_call_batch` is final and there is no
|
|
994
|
+
delete — a batch is the record of money spent. `add_call_batch_recipients`
|
|
995
|
+
appends to a running batch and silently skips numbers it already holds, so
|
|
996
|
+
re-uploading an exported list never calls anyone twice.
|
|
997
|
+
|
|
998
|
+
**Watching it.** `get_call_batch` has live counts and `failure_reason`; a batch
|
|
999
|
+
that stops itself did so after ten consecutive setup failures (an unpublished
|
|
1000
|
+
agent, an expired carrier credential, a trunk refusing everything).
|
|
1001
|
+
`list_calls` with `batch_id` gives the calls themselves — transcripts,
|
|
1002
|
+
recordings and cost.
|
|
1003
|
+
|
|
1004
|
+
## Email outbound
|
|
1005
|
+
|
|
1006
|
+
To email a *list* rather than one person, create an **email batch**: a CSV, an
|
|
1007
|
+
agent task that drafts each row, a connected Resend account, and a mapping that
|
|
1008
|
+
says which column carries the address, the subject and the body.
|
|
1009
|
+
|
|
1010
|
+
**Four things to say before creating one.** A user who discovers any of them
|
|
1011
|
+
afterwards will not forgive us:
|
|
1012
|
+
|
|
1013
|
+
- **Resend's terms prohibit unsolicited email, cold outreach, purchased lists
|
|
1014
|
+
and scraped contact data**, and require that every recipient has explicitly
|
|
1015
|
+
opted in. This runs on *their* Resend account under *their* agreement.
|
|
1016
|
+
- **Nothing is sent that they have not reviewed and selected.** Creating a batch
|
|
1017
|
+
only drafts.
|
|
1018
|
+
- **Every row runs the task as it stands right now.** Editing it while a batch
|
|
1019
|
+
is drafting changes every row generated after that moment, and there is no way
|
|
1020
|
+
to get the old wording back.
|
|
1021
|
+
- **Bounces and spam complaints are only visible if they point a Resend webhook
|
|
1022
|
+
at this workspace.** The URL is on the integration's page.
|
|
1023
|
+
|
|
1024
|
+
### The column space
|
|
1025
|
+
|
|
1026
|
+
A row has **one flat column space, filled from two directions**: the CSV
|
|
1027
|
+
supplies some columns, the task's `output` supplies the rest, and a person's
|
|
1028
|
+
edits sit on top. `field_map` points `to`, `subject` and `body` at names in that
|
|
1029
|
+
merged space, and each may name **either** a CSV header or a field the task
|
|
1030
|
+
produces. That is what makes "the CSV already has addresses" and "the CSV has
|
|
1031
|
+
phone numbers and the task looks the address up" the same feature.
|
|
1032
|
+
|
|
1033
|
+
A row is sendable when all three resolve to a non-empty string and `to` parses
|
|
1034
|
+
as an address. Every other field the task produces is kept on the row verbatim
|
|
1035
|
+
as context and read by nobody.
|
|
1036
|
+
|
|
1037
|
+
Refused at create, all at once: a mapped name that is neither a column nor an
|
|
1038
|
+
output field; a mapped output field that is not a `string`; a CSV header that
|
|
1039
|
+
collides with an output field name (one of the two would be unreachable); a
|
|
1040
|
+
`required` variable the task declares that has no column and no default; a
|
|
1041
|
+
`from_email` whose domain is not verified on that Resend account right now.
|
|
1042
|
+
|
|
1043
|
+
**A header that exists but is blank on some rows still passes create** and fails
|
|
1044
|
+
those rows at draft time with `missing_vars` — one row, not the batch, and with
|
|
1045
|
+
no tokens spent. Flag blank cells in a required column before creating.
|
|
1046
|
+
|
|
1047
|
+
### Two jobs, and a human between them
|
|
1048
|
+
|
|
1049
|
+
**Drafting** starts on `start_at` (or immediately), honours `calling_window` on
|
|
1050
|
+
the batch's `timezone`, runs `max_concurrency` rows at a time, and stops when
|
|
1051
|
+
the last row lands. Its `status` is about drafting and nothing else —
|
|
1052
|
+
`scheduled`, `drafting`, `paused`, `drafted`, `canceled`, `failed`. A `drafted`
|
|
1053
|
+
batch means *"the drafts are ready to review"*, never "finished": it says
|
|
1054
|
+
nothing about what has been sent.
|
|
1055
|
+
|
|
1056
|
+
**Sending** is separate and only ever happens when a person asks.
|
|
1057
|
+
`send_email_batch_recipients` takes **at most 50 rows, named one by one**. There
|
|
1058
|
+
is no `selection` shorthand on it, no bulk endpoint, and there must not be one:
|
|
1059
|
+
a 5 000-row batch cannot leave the building without a hundred deliberate calls,
|
|
1060
|
+
each of which should follow reading the fifty drafts it covers. That friction is
|
|
1061
|
+
the only structural safeguard this feature has — **do not offer to work around
|
|
1062
|
+
it.** `skip`, `restore` and `retry` *do* take `selection: "all_eligible"`,
|
|
1063
|
+
because they are reversible and mailing strangers is not.
|
|
1064
|
+
|
|
1065
|
+
Each send may override `from_email` / `from_name` / `reply_to` for that call
|
|
1066
|
+
alone; the batch's own default is unchanged. The Resend account itself is not
|
|
1067
|
+
overridable — a different account is a different batch.
|
|
1068
|
+
|
|
1069
|
+
### Fixing rows
|
|
1070
|
+
|
|
1071
|
+
`list_email_batch_recipients` returns each row's `columns` (the merged space),
|
|
1072
|
+
its three halves separately, and `not_ready_reason` — why it cannot be sent yet,
|
|
1073
|
+
null when it can. `patch_email_batch_recipient` edits cells: the edit is stored
|
|
1074
|
+
apart from what the model wrote, merged last, and an empty value clears it. That
|
|
1075
|
+
is the answer to a row whose task returned `null` for the address — type one in.
|
|
1076
|
+
|
|
1077
|
+
`retry_email_batch_recipients` re-drafts rows whose drafting failed, and is the
|
|
1078
|
+
one action that revives a batch that already finished drafting.
|
|
1079
|
+
|
|
1080
|
+
**One address is mailed at most once per batch**, enforced when the row is
|
|
1081
|
+
claimed for sending: the second row settles `skipped` with
|
|
1082
|
+
`duplicate_recipient`. There is no cross-batch dedup and no suppression list of
|
|
1083
|
+
ours — Resend keeps one and auto-suppresses hard bounces and complaints.
|
|
1084
|
+
|
|
1085
|
+
### Watching it
|
|
1086
|
+
|
|
1087
|
+
`get_email_batch` carries live counts, `failure_reason`, `next_draft_at` (which
|
|
1088
|
+
is how "waiting for Monday" is told apart from "stuck") and the drafting cost so
|
|
1089
|
+
far. A batch that stops itself did so after ten consecutive drafting failures,
|
|
1090
|
+
or on the first `configuration` failure (a deleted task, a field map an edit
|
|
1091
|
+
broke) or the first account-level send failure (a revoked key, an unverified
|
|
1092
|
+
domain) — because those will fail identically for every remaining row.
|
|
1093
|
+
|
|
1094
|
+
`delivery_status` on a row is Resend's verdict, separate from ours: `sent` is
|
|
1095
|
+
what we did, `delivered` / `bounced` / `complained` is what happened next.
|
|
1096
|
+
|
|
1097
|
+
## Knowledge bases
|
|
1098
|
+
|
|
1099
|
+
A knowledge base gives an agent grounded answers over crawled content. Attach
|
|
1100
|
+
ready ones through the agent's `kb_ids`; only `ready` ones can be attached.
|
|
1101
|
+
|
|
1102
|
+
Building one runs in three stages and takes minutes, not seconds:
|
|
1103
|
+
|
|
1104
|
+
1. `create_kb` with the site URLs starts the crawl. It returns immediately at
|
|
1105
|
+
`discovering` — poll `get_kb` until `status` is `review`.
|
|
1106
|
+
2. At `review`, `discovered` holds every URL the crawl found. Choose the ones
|
|
1107
|
+
worth indexing and pass them to `build_kb`. This choice decides the quality
|
|
1108
|
+
of the whole knowledge base: prefer pages that answer real questions over
|
|
1109
|
+
everything the crawl happened to reach.
|
|
1110
|
+
3. The KB moves through `transcribing` and `generating` to `ready`. Keep
|
|
1111
|
+
polling `get_kb`; `progress` says what stage it is in and `error` says if it
|
|
1112
|
+
stopped.
|
|
1113
|
+
|
|
1114
|
+
Once ready, `get_kb_prompt` is the table of contents exactly as the agent
|
|
1115
|
+
receives it — read it to judge what the agent will actually be able to find,
|
|
1116
|
+
since a vague entry is one it will never fetch. `list_pages` shows which URLs
|
|
1117
|
+
failed to transcribe, `patch_node` corrects an entry by hand, and
|
|
1118
|
+
`regenerate_toc` rebuilds the structure without crawling again.
|
|
1119
|
+
|
|
1120
|
+
Tell the user a build is running rather than waiting silently through it, and
|
|
1121
|
+
never poll a build in a tight loop.
|
|
1122
|
+
|
|
1123
|
+
## Secrets, keys and webhooks
|
|
1124
|
+
|
|
1125
|
+
**Secrets** are workspace values referenced as `{{secrets.NAME}}` from tool
|
|
1126
|
+
operations and integration config. They are write-only: once stored, only the
|
|
1127
|
+
runtime resolves them.
|
|
1128
|
+
|
|
1129
|
+
**BYOK.** Talqing runs agents on the workspace's own provider API keys and has
|
|
1130
|
+
none of its own. `list_provider_keys` says which providers are configured, and
|
|
1131
|
+
every key it lists was accepted by that provider when it was saved —
|
|
1132
|
+
`set_provider_key` calls the provider first and stores nothing it rejects.
|
|
1133
|
+
Prefer models from configured providers; if the user wants one that is not
|
|
1134
|
+
there, tell them the key has to be added before the agent can publish.
|
|
1135
|
+
|
|
1136
|
+
**Webhooks** are workspace-wide and fire for every agent. `event_types` lists
|
|
1137
|
+
what can be subscribed to, and `webhook_deliveries` is where to look when one
|
|
1138
|
+
seems silent.
|
|
1139
|
+
|
|
1140
|
+
## Running an agent, and reading what happened
|
|
1141
|
+
|
|
1142
|
+
Published agents can be run from here, and every run leaves a record.
|
|
1143
|
+
|
|
1144
|
+
**Starting something.** `create_outbound_call` dials a real phone number from
|
|
1145
|
+
one of the workspace's numbers and puts a published voice agent on the call.
|
|
1146
|
+
`create_text_conversation` plus `create_text_message` talks to a published text
|
|
1147
|
+
agent — the fastest way to try one. `calls_token` mints the token a *browser*
|
|
1148
|
+
needs to join a web call; it starts nothing by itself.
|
|
1149
|
+
|
|
1150
|
+
All three take `userdata`, which seeds the session's state and is readable as
|
|
1151
|
+
`{{userdata.field}}` from the agent's prompt, greeting and tools. That is how
|
|
1152
|
+
you pass in who is being reached and why — one published agent, personalized
|
|
1153
|
+
per call. Keys beginning with `_talqing` are reserved.
|
|
1154
|
+
|
|
1155
|
+
All three — and `create_call_batch` — also take `vars`, a flat `{name: value}`
|
|
1156
|
+
map of strings read as `{{vars.name}}`. It overrides the declared defaults on
|
|
1157
|
+
every agent the session runs and is gone when the session ends. Use `userdata`
|
|
1158
|
+
for facts about the person and `vars` for configuration of the session; unlike
|
|
1159
|
+
`userdata`, `vars` is never written onto the caller's contact record and no tool
|
|
1160
|
+
can change it mid-session. An empty string is a deliberate blank, not a request
|
|
1161
|
+
for the default. The model can see the values, so credentials stay in workspace
|
|
1162
|
+
secrets. On a batch it is one bag for the whole campaign, copied onto every call
|
|
1163
|
+
it places — per-person data is what a recipient's `userdata` is for. On a text
|
|
1164
|
+
conversation it is fixed for the life of the thread.
|
|
1165
|
+
|
|
1166
|
+
**Per-call configuration.** All three — and `create_call_batch` — also take four
|
|
1167
|
+
fields that change what runs on that one call:
|
|
1168
|
+
|
|
1169
|
+
- `agent_version` — pin a version, or `"draft"` to run the unpublished working
|
|
1170
|
+
copy. That is how you try an edit without publishing over what live callers
|
|
1171
|
+
are hearing.
|
|
1172
|
+
- `agent_override` — an `AgentConfig` with only the fields this call changes.
|
|
1173
|
+
Absent keys keep the published value, an explicit `null` clears a field,
|
|
1174
|
+
objects deep-merge and lists replace wholesale. So `{"tts": {"voice":
|
|
1175
|
+
"aditi"}}` changes the voice and leaves the provider and model alone. Note
|
|
1176
|
+
that `agent_override.vars` is a list of *declarations* and replaces them
|
|
1177
|
+
wholesale, while the top-level `vars` above supplies *values*.
|
|
1178
|
+
- `agent` — a whole agent definition, run for this call and stored nowhere.
|
|
1179
|
+
- `agent_team` — `{"members": [{name, …the same four fields…}]}`, a cast of
|
|
1180
|
+
agents for one call. **`members[0]` answers**, and members reach each other
|
|
1181
|
+
through `handoffs` entries that name them.
|
|
1182
|
+
|
|
1183
|
+
`agent_id`/`agent`/`agent_version`/`agent_override` and `agent_team` are
|
|
1184
|
+
mutually exclusive: one agent, or a team of them.
|
|
1185
|
+
|
|
1186
|
+
**Prefer `agent_id` and `userdata`.** Reach for `agent_override` when one call
|
|
1187
|
+
genuinely differs from the published agent, and for `agent` or `agent_team` only
|
|
1188
|
+
when the definition is generated per request and would never be reused. An agent
|
|
1189
|
+
that exists in the workspace has an editor, a version history, a diff and reuse
|
|
1190
|
+
across calls; an inline one has none of those and disappears with the call.
|
|
1191
|
+
Composing an inline team by habit ends with a workspace that has no agents in it.
|
|
1192
|
+
|
|
1193
|
+
The merged result is validated by the same rules that guard publishing, so a
|
|
1194
|
+
bad override is a 400 at create — naming the problem — rather than a call that
|
|
1195
|
+
connects and then fails.
|
|
1196
|
+
|
|
1197
|
+
`create_text_message` returns as soon as the message is accepted — the agent
|
|
1198
|
+
replies asynchronously, so poll `list_conversation_items` (with `order: desc`)
|
|
1199
|
+
for the reply. Give it a few seconds, and expect several items when the agent
|
|
1200
|
+
calls tools.
|
|
1201
|
+
|
|
1202
|
+
It can also carry `images`: up to four `{data_url, filename}` entries beside
|
|
1203
|
+
`message`, each a base64 `data:image/...` URL of a JPEG, PNG or WebP under 10 MB.
|
|
1204
|
+
The caption and the photo arrive as one turn and one item, so send them together
|
|
1205
|
+
rather than as two messages. The agent's model must have `vision` (see *Images*
|
|
1206
|
+
above) or the call is refused naming the model.
|
|
1207
|
+
|
|
1208
|
+
**Reading it back.** A *conversation* is the thread with a person, on any
|
|
1209
|
+
surface; a *call* is one voice or video session. `list_conversations` and
|
|
1210
|
+
`list_conversation_items` are the transcript; `list_conversation_sessions` says
|
|
1211
|
+
which agent version handled each turn of it. `list_calls` and `get_call` cover
|
|
1212
|
+
phone and web calls — `get_call` gives the transcript with every tool call and
|
|
1213
|
+
its output, plus usage, cost and a per-stage latency breakdown, which is where
|
|
1214
|
+
you find out why an agent did something odd or answered slowly.
|
|
1215
|
+
`get_observability` is the workspace view: sessions, spend and latency per day.
|
|
1216
|
+
|
|
1217
|
+
What a run costs is the provider spend on the workspace's own keys plus
|
|
1218
|
+
Talqing's per-minute platform fee, which `get_catalog` gives per channel. **Text
|
|
1219
|
+
agents carry no platform fee at all** — a text conversation costs the workspace
|
|
1220
|
+
only what its own LLM key was charged.
|
|
1221
|
+
|
|
1222
|
+
When an agent misbehaves, read the actual run before theorizing. The transcript
|
|
1223
|
+
and the final `userdata` usually name the cause.
|
|
1224
|
+
|
|
1225
|
+
## Rules
|
|
1226
|
+
|
|
1227
|
+
- **Never publish an agent unless asked.** Propose it when the draft is ready
|
|
1228
|
+
and wait for the user to say publish, deploy or go live. Publishing a *tool*
|
|
1229
|
+
needs no confirmation — it is a prerequisite for attaching it.
|
|
1230
|
+
- **Never reach a real person unless asked for that specific contact.** An
|
|
1231
|
+
outbound call rings someone's phone and costs money; a text message to a
|
|
1232
|
+
connected channel is a message they receive. Both are irreversible. Test
|
|
1233
|
+
against a number or thread the user has named, never one you picked.
|
|
1234
|
+
- **Delete only on an explicit request naming the target.** Agents, tools,
|
|
1235
|
+
knowledge bases, carrier accounts, integrations, triggers, secrets, webhooks
|
|
1236
|
+
and tokens are all permanent. Taking a phone number out of service stops real
|
|
1237
|
+
callers from reaching anyone — treat it the same way.
|
|
1238
|
+
- **Never echo a secret back.** Personal access tokens, webhook signing secrets
|
|
1239
|
+
and provider keys are shown once; repeating one puts it in the conversation
|
|
1240
|
+
transcript.
|
|
1241
|
+
- **Validate before you claim something is ready.** `validate_tool` after tool
|
|
1242
|
+
changes, `validate_agent` after config, hook or handoff changes. Treat errors
|
|
1243
|
+
as work to do and summarize warnings plainly. Validation runs on the server
|
|
1244
|
+
against rules you cannot check by reading a draft, so **never report a
|
|
1245
|
+
validation result you did not get back from the operation**, and never answer
|
|
1246
|
+
a request to validate by reasoning about the config instead of calling it.
|
|
1247
|
+
- **Do not invent fields.** Author tools and configs through the exact schemas
|
|
1248
|
+
the operations declare. A field the schema does not have is rejected. An
|
|
1249
|
+
argument whose description ends in `describe_schema('X')` carries a
|
|
1250
|
+
placeholder instead of its shape — fetch it before writing one, never
|
|
1251
|
+
reconstruct it from memory.
|
|
1252
|
+
- **Say what the platform does not do.** Decline capabilities that do not exist
|
|
1253
|
+
yet rather than approximating them.
|
|
1254
|
+
|
|
1255
|
+
## Writing agent prompts
|
|
1256
|
+
|
|
1257
|
+
The agent prompt is the deliverable that decides whether the agent is any good.
|
|
1258
|
+
Give it a role, a goal, the rules it must follow, when to use which tool, and
|
|
1259
|
+
the language and tone to use.
|
|
1260
|
+
|
|
1261
|
+
For voice and video, write for speech, not for a document:
|
|
1262
|
+
|
|
1263
|
+
- Natural, brief and warm. Easy to interrupt.
|
|
1264
|
+
- Never dump long information — give the next useful chunk and ask whether to go
|
|
1265
|
+
on.
|
|
1266
|
+
- No markdown, emoji, raw JSON or list formatting in what the agent says.
|
|
1267
|
+
- Text-to-speech mangles abbreviations, symbols and numbers; spell out the forms
|
|
1268
|
+
that matter, or give pronunciation rules.
|
|
1269
|
+
- Make tool use explicit: when to call each tool, what to collect first, and
|
|
1270
|
+
what to say while waiting.
|