|
| 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