Skip to content

Commit 3887bf0

Browse files
authored
feat: add "Passing data between assistants" page to squads (#1033)
## Description - New page: `fern/squads/passing-data-between-assistants.mdx` — a focused decision guide for the three mechanisms Vapi supports for forwarding context across squad handoffs. - `fern/docs.yml` updated: new entry under the **Build → Squads** section, sitting between the existing **Handoff tool** page and the **Examples** subsection. The existing `/squads/handoff` page covers configuration mechanics for the handoff tool itself but doesn't compare the three approaches (`function.parameters`, `variableExtractionPlan.schema`, Liquid in the destination prompt) as alternatives. Customers default to schema extraction even when handoff arguments or direct Liquid would be faster, cheaper, and more reliable. The page covers: - At-a-glance comparison table (latency, hallucination risk, where the value comes from) - Per-approach example + when-to-use / when-to-avoid - Decision flowchart for choosing the right mechanism - Common patterns: forwarding extracted IDs, classifying intent, structured booking requests, mix-and-match - Failure-mode notes (empty plan, LLM rejection, non-object result) reflecting the platform-hardening guarantees in [vapi#11302](VapiAI/vapi#11302) ([PRISM-467](https://linear.app/vapi/issue/PRISM-467)) Resolves the docs follow-up tracked alongside [PRISM-467](https://linear.app/vapi/issue/PRISM-467). ## Testing Steps - [x] Run `fern check` — passes with 0 errors (5 pre-existing warnings, unrelated) - [x] Verify JSON examples are valid — 2 JSON blocks, both parse cleanly - [x] Verify internal links resolve — 5 links: `/squads/handoff`, `/squads/handoff#variable-extraction`, `/squads/handoff` (again), `/tools/static-variables-and-aliases`, `/assistants/dynamic-variables` — all OK - [x] New page added to `fern/docs.yml` between **Handoff tool** and **Examples** in the Squads section - [ ] Confirm the page renders at `/squads/passing-data-between-assistants` in the preview deployment - [ ] Confirm sidebar position appears between Handoff tool and Examples - [x] Style guide compliance — active voice, present tense, second person, no marketing language, key terms bolded on first use - [x] Code examples use realistic placeholders (`Specialist`, `Scheduler`) and reference actual JSON schemas matching the API - [x] No commit trailer added
1 parent fdbe0bc commit 3887bf0

2 files changed

Lines changed: 189 additions & 0 deletions

File tree

fern/docs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ navigation:
341341
- page: Handoff tool
342342
path: squads/handoff.mdx
343343
icon: fa-light fa-hand-holding-hand
344+
- page: Passing data between assistants
345+
path: squads/passing-data-between-assistants.mdx
344346
- section: Examples
345347
icon: fa-light fa-code
346348
contents:
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
---
2+
title: Passing data between assistants
3+
subtitle: Three approaches for forwarding context to the next assistant in a squad — when to use each, and what each one costs.
4+
slug: squads/passing-data-between-assistants
5+
---
6+
7+
When an assistant in a squad hands off to another assistant, you usually need to forward something — the caller's name, an extracted intent, an upstream tool's result, a session ID. Vapi gives you **three different mechanisms** to do this. Each one trades off latency, accuracy, and where the value comes from. Picking the wrong one is the single most common reason squad handoffs feel slow or unreliable.
8+
9+
This page is a decision guide. For end-to-end configuration of the handoff itself, see the [Handoff tool](/squads/handoff) page.
10+
11+
## The three approaches at a glance
12+
13+
| Approach | Where the value comes from | LLM involved? | Latency | Hallucination risk | Best for |
14+
| -------- | -------------------------- | ------------- | ------- | ------------------ | -------- |
15+
| **Handoff arguments** (`function.parameters` on the handoff tool) | The model decides, inline with the same tool call that triggers the handoff | Yes — piggybacks on the LLM call already happening | Zero added | Yes (model fills the value) | Classifications, summaries, sentiment, intent — anything the model has to derive from the live conversation |
16+
| **Variable extraction** (`variableExtractionPlan.schema` on the destination) | The model extracts from the full conversation transcript | Yes — separate dedicated LLM call | Full LLM round-trip (hundreds of ms) | Yes | Structured extraction with a dedicated prompt — e.g. pulling `dateOfBirth`, `appointmentTime` from the user's last few utterances |
17+
| **Liquid templating in the destination's prompt** | Already in the variable bag (call data, prior tool results, prior extractions) | No — pure template substitution | Sub-millisecond per render | No (deterministic) | Forwarding values that already exist — caller phone number, prior `lookupPatient` result, time variables |
18+
19+
## Approach 1: Handoff arguments
20+
21+
Define `function.parameters` on the handoff tool. The LLM that's already generating the handoff tool call also fills in your custom arguments as part of the same call — no extra round-trip.
22+
23+
<Warning>
24+
**Availability today:**
25+
26+
- **API:** Fully supported. Send the JSON below via `POST /tool` or as part of your assistant's `model.tools[]` via `POST /assistant` / `PATCH /assistant`.
27+
- **Dashboard — Tools page:** UX for defining `function.parameters` on a handoff tool is shipping soon. Use the API in the meantime.
28+
- **Dashboard — Squad builder:** Configuring a handoff via the squad member's **Handoff Tools** section does NOT currently carry `function.parameters` through to the runtime tool (backend synthesizes the tool without the `function` field). Until that's fixed, put the handoff tool directly on the assistant's `model.tools[]` (via the API or the Tools page) instead of defining it per squad-member destination.
29+
</Warning>
30+
31+
32+
33+
```json
34+
{
35+
"type": "handoff",
36+
"function": {
37+
"name": "handoff_to_specialist",
38+
"description": "Hand off to the specialist when the customer is ready",
39+
"parameters": {
40+
"type": "object",
41+
"required": ["destination", "customerIntent", "customerSentiment"],
42+
"properties": {
43+
"destination": {
44+
"type": "string",
45+
"enum": ["specialist"]
46+
},
47+
"customerIntent": {
48+
"type": "string",
49+
"enum": ["new-customer", "existing-customer", "billing-issue"],
50+
"description": "What the customer is calling about"
51+
},
52+
"customerSentiment": {
53+
"type": "string",
54+
"enum": ["positive", "neutral", "frustrated"],
55+
"description": "Caller's overall sentiment"
56+
}
57+
}
58+
}
59+
},
60+
"destinations": [
61+
{
62+
"type": "assistant",
63+
"assistantName": "Specialist"
64+
}
65+
]
66+
}
67+
```
68+
69+
The next assistant receives `customerIntent` and `customerSentiment` in the variable bag and can reference them as `{{customerIntent}}` / `{{customerSentiment}}` in its prompts.
70+
71+
**Use this when** the value only exists "in the model's head" — it has to be derived from the live conversation, but you don't need a separate dedicated extraction call.
72+
73+
**Avoid this when** the value already exists somewhere structured (a prior tool result, the call's `customer.number`, etc.) — the model could mishear or paraphrase it. Use Approach 3 for those.
74+
75+
## Approach 2: Variable extraction (`variableExtractionPlan.schema`)
76+
77+
Define a `variableExtractionPlan.schema` on the handoff destination. After the handoff fires, Vapi makes a dedicated LLM call against the full conversation transcript to fill the schema, then merges the result into the variable bag for the next assistant.
78+
79+
```json
80+
{
81+
"type": "assistant",
82+
"assistantName": "Scheduler",
83+
"variableExtractionPlan": {
84+
"schema": {
85+
"type": "object",
86+
"required": ["preferredDate", "preferredTime"],
87+
"properties": {
88+
"preferredDate": {
89+
"type": "string",
90+
"description": "The date the caller asked to schedule for, in YYYY-MM-DD format"
91+
},
92+
"preferredTime": {
93+
"type": "string",
94+
"description": "The time of day the caller asked for, in 24-hour HH:MM format"
95+
}
96+
}
97+
}
98+
}
99+
}
100+
```
101+
102+
**Use this when** the value lives across several user utterances and needs a dedicated extraction prompt to get reliably. Schema validation gives you typed output and lets you constrain values via JSON-schema `enum` / `pattern`.
103+
104+
**Avoid this when** zero added latency matters — this path adds a full LLM round-trip per handoff (typically a few hundred ms). For high-traffic flows where the value is something the model can fill inline, Approach 1 is faster.
105+
106+
For full configuration details — multiple destinations, dynamic handoffs, context engineering — see the [Variable extraction section of the Handoff tool page](/squads/handoff#variable-extraction).
107+
108+
## Approach 3: Liquid templating in the destination's prompt
109+
110+
The variable bag is **shared across every assistant in the squad** for the lifetime of the call. Anything that's been put into it — by Approach 1, Approach 2, by a prior tool call returning JSON, by call-level data like `customer.number` and `phoneNumber.number`, by time variables like `now` and `year` — is reachable from any subsequent assistant's prompt via Liquid syntax. No extra wiring required.
111+
112+
```text
113+
You are the scheduling specialist. The caller is {{customer.name}}, calling
114+
from {{customer.number}}. Their patient ID is {{patientId}} (looked up earlier
115+
this call). They want a {{preferredAppointmentType}} appointment.
116+
117+
Today is {{currentDateTime}}.
118+
```
119+
120+
If `customer.name`, `patientId`, etc. are in the bag, they render. If they're not, they render as the literal token `{{patientId}}` (so the caller might hear "patientId" spoken — worth handling defensively in your prompt).
121+
122+
**Use this when** the value is already in the bag — there's no reason to re-extract via LLM what you already have structurally. Sub-millisecond, deterministic, free.
123+
124+
**Avoid this when** the value isn't in the bag yet. Liquid can't extract from the conversation; it can only forward what's already there.
125+
126+
<Note>
127+
**Sensitive fields are sanitized.** Vapi automatically redacts credential-like keys (`twilioAuthToken`, `twilioApiSecret`, `serverUrlSecret`, `accountSid`, `callToken`, `credentialId`, etc.) from the variable bag before any prompt rendering. References like `{{phoneNumber.twilioAuthToken}}` will render as `[REDACTED]` rather than leaking the actual credential.
128+
</Note>
129+
130+
## Decision flowchart
131+
132+
```text
133+
What do you want the next assistant to know?
134+
135+
├─ "Something the model just heard / classified / summarized"
136+
│ └─→ Approach 1: Handoff arguments
137+
│ Zero added latency, model fills inline.
138+
139+
├─ "Something the user explicitly said and I want a dedicated, schema-validated extraction"
140+
│ └─→ Approach 2: variableExtractionPlan.schema
141+
│ Adds an LLM round-trip, but you get structured output and a focused
142+
│ extraction prompt.
143+
144+
└─ "Something I already have — call data, prior tool result, prior extraction"
145+
└─→ Approach 3: Reference it via Liquid in the destination's prompt
146+
No extra cost. Use {{customer.number}}, {{patientId}}, etc. directly.
147+
```
148+
149+
## Common patterns
150+
151+
### Pattern: "Forward an extracted ID after a database lookup"
152+
153+
A `lookupPatient` tool returned `{patientId: "p_42", dob: "1990-01-15"}` on assistant A. Assistant B needs `patientId`.
154+
155+
Use **Approach 3** — it's already in the bag. Assistant B's prompt: `The patient ID is {{patientId}}.` Don't re-extract it via schema; the model could mishear digits.
156+
157+
### Pattern: "Categorize what the caller wants and route on it"
158+
159+
Caller spent two turns describing a problem. Assistant A needs to classify the intent and hand off to a specialist who knows about that intent.
160+
161+
Use **Approach 1** — handoff arguments with an `enum` for `intent`. The classifying assistant's tool call carries the intent inline; the destination assistant reads `{{intent}}`.
162+
163+
### Pattern: "Pull a structured booking request out of free-form speech"
164+
165+
Caller said "I want to come in next Tuesday around 2 PM, maybe earlier if there's something". Assistant A needs `{preferredDate, preferredTime, alternativesOK}` as structured fields.
166+
167+
Use **Approach 2**`variableExtractionPlan.schema` with the destination. The dedicated extraction prompt + schema validation catches the structure better than inline arguments.
168+
169+
### Pattern: "Mix and match"
170+
171+
You can combine all three on a single handoff. Common shape: handoff arguments for the LLM-classified intent, schema extraction for one structured field that needs the dedicated prompt, and the destination's system prompt directly references prior tool results via Liquid.
172+
173+
## What if extraction fails?
174+
175+
Vapi's handoff path is failure-isolated:
176+
177+
- An empty `variableExtractionPlan` (`{}`) is a graceful no-op — the handoff proceeds without extraction.
178+
- A schema-extraction LLM failure (5xx, timeout, rate limit) is logged and the handoff proceeds with no extracted variables — it does not bail the handoff.
179+
- A schema-extraction result that isn't a plain object (an array, a primitive, `null`) is dropped before merge — it does not corrupt the variable bag.
180+
181+
So extraction is best-effort; if values are critical for the next assistant to function, prefer **Approach 1** (handoff arguments — required by the function schema, blocks the LLM call until provided) or **Approach 3** (reference values you already have).
182+
183+
## Next steps
184+
185+
- [Handoff tool](/squads/handoff) — full configuration reference for the handoff tool itself.
186+
- [Static variables and aliases](/tools/static-variables-and-aliases) — how the variable bag is built and what's available in scope.
187+
- [Dynamic variables](/assistants/dynamic-variables) — set initial variables when starting a call.

0 commit comments

Comments
 (0)