diff --git a/fern/squads/handoff/context-and-variables.mdx b/fern/squads/handoff/context-and-variables.mdx index ee57233ec..5fdf696d6 100644 --- a/fern/squads/handoff/context-and-variables.mdx +++ b/fern/squads/handoff/context-and-variables.mdx @@ -1,34 +1,32 @@ --- title: Pass context and variables -subtitle: Control which conversation history and extracted variables reach the next assistant -description: Choose how much conversation history the receiving assistant sees, and extract variables so structured data follows the caller into the next assistant. +subtitle: Control the history and values available after a handoff +description: Choose the conversation history a destination receives and pass variables through handoff arguments, tool results, or extraction. slug: squads/handoff/context-and-variables --- -When a call moves to another assistant, you control what travels with it. Context engineering decides how much conversation history the receiving assistant can see, and variable extraction captures specific values and forwards them explicitly. +A handoff carries **conversation messages** and **variable values** separately. Set `contextEngineeringPlan` on the destination to choose which earlier messages its model sees. The destination gets its own system prompt in every mode. -## Context engineering - -Control what conversation history transfers to the next assistant or squad. Set **contextEngineeringPlan** on any destination. - -### All messages (default) +Values already collected in the call remain available to later assistants even if the destination receives no earlier messages. -Transfers the entire conversation history: +## Context engineering -```json -{ - "contextEngineeringPlan": { - "type": "all" - } -} -``` +Set `contextEngineeringPlan` on an assistant or squad destination. It filters the messages sent to that destination; it does not clear saved variables. -### Last N messages +| Type | Earlier messages the destination receives | +| --- | --- | +| `all` (default) | All earlier conversation messages, including tool calls and results. The previous system prompt is replaced by the destination's system prompt. | +| `lastNMessages` | The most recent `maxMessages` messages. Use a positive value: `0` currently selects all messages. | +| `userAndAssistantMessages` | User and assistant messages; tool calls and tool results are excluded. | +| `previousAssistantMessages` | Messages from before the current assistant received its incoming handoff. On the first handoff, there is no earlier assistant context to carry. | +| `none` | No earlier conversation messages. | -Transfers only the most recent N messages. Use this to limit context size for performance: +For example, this destination receives the ten most recent messages: ```json { + "type": "assistant", + "assistantName": "Specialist", "contextEngineeringPlan": { "type": "lastNMessages", "maxMessages": 10 @@ -36,226 +34,104 @@ Transfers only the most recent N messages. Use this to limit context size for pe } ``` -### User and assistant messages only - -Transfers only user and assistant messages, filtering out system messages, tool calls, and tool results. This gives the next assistant a clean view of the conversation without internal implementation details: - -```json -{ - "contextEngineeringPlan": { - "type": "userAndAssistantMessages" - } -} -``` + +`previousAssistantMessages` and `none` filter the destination model's message history. They do not erase values already in the variable bag or remove messages from a full-history call artifact. If you handle sensitive information, control what is collected, stored, extracted, and passed in variables as well as message history. + - -Use `userAndAssistantMessages` when the destination assistant does not need to see tool call history or system prompts from the previous assistant. This produces a cleaner context and reduces token usage. - - -### Previous assistant messages - -Transfers only the conversation history from **before** the current assistant's session. This excludes the current assistant's own messages, tool calls, and tool results entirely, forwarding only the context that existed when the current assistant first received the call. +## Variable extraction -```json -{ - "contextEngineeringPlan": { - "type": "previousAssistantMessages" - } -} -``` +Choose how a value reaches the destination based on its source: -This mode is particularly useful when the current assistant handles sensitive data (such as payment card numbers in a PCI-compliant flow). By excluding the current assistant's session from the forwarded context, you prevent sensitive tool call results from reaching the next assistant. +| Value source | When it runs | What reaches the destination | +| --- | --- | --- | +| Standard call and time values | Vapi makes supported values available for the call or channel. See [Dynamic variables](/assistants/dynamic-variables). | Available values such as `customer.number`, `call.id`, and `now` can render in the prompt. | +| Initial custom values | Supply `assistantOverrides.variableValues` when starting the call. | Those named values can render in the destination prompt. | +| Earlier tool response | Configure `variableExtractionPlan.aliases` on the **result-producing tool**; it selects fields when the tool returns. | Only selected alias values are saved. A JSON response alone is not copied into the variable bag. | +| Handoff arguments | Define custom `function.parameters` on the handoff tool; the model fills them in the outgoing handoff tool call. | Every argument except `destination` is added to the bag. | +| Conversation content | Define `variableExtractionPlan.schema` on the destination; a separate model request runs at handoff. | Top-level properties in the extracted object are added to the bag if extraction succeeds. | +| Existing bag value | Liquid runs when the destination prompt is rendered. | A reference such as `{{customerName}}` reads the current value; it does not extract one. | - -Use `previousAssistantMessages` when handing off from a sensitive assistant (e.g., one collecting payment data) to a non-sensitive assistant. It preserves useful conversation context from earlier in the call while ensuring the sensitive assistant's tool call data is not forwarded. See the [PCI Compliance - Handoff Context Configuration](/security-and-privacy/pci#handoff-context-configuration) guide for a complete walkthrough. - +At handoff, Vapi adds tool arguments, then any schema-extracted values, then the chosen destination's `assistantOverrides.variableValues` or `squadOverrides.variableValues`. If the same key is set more than once, the later value wins. -### No context +### Handoff arguments -Starts the next assistant with a blank conversation: +Use `function.parameters` when the outgoing assistant can provide a value as it decides to hand off. This tool passes `customerIntent` to `Specialist`: ```json { - "contextEngineeringPlan": { - "type": "none" - } -} -``` - -## Variable extraction - -Extract and pass structured data during handoff. Variables extracted by the Handoff tool are available to all subsequent assistants in the conversation chain. When a handoff extracts a variable with the same name as an existing one, the new value replaces the previous value. - -### Extraction via `variableExtractionPlan` in destinations - -This extraction method makes an OpenAI structured output request to extract variables. Use this when you have multiple destinations, each with different variables that need to be extracted. - -```json -{ - "tools": [ + "type": "handoff", + "destinations": [ { - "type": "handoff", - "destinations": [ - { - "type": "assistant", - "assistantName": "order-processing-assistant", - "description": "customer is ready to place an order", - "variableExtractionPlan": { - "schema": { - "type": "object", - "properties": { - "customerName": { - "type": "string", - "description": "Full name of the customer" - }, - "email": { - "type": "string", - "format": "email", - "description": "Customer's email address" - }, - "productIds": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of product IDs customer wants to order" - }, - "shippingAddress": { - "type": "object", - "properties": { - "street": { "type": "string" }, - "city": { "type": "string" }, - "state": { "type": "string" }, - "zipCode": { "type": "string" } - } - } - }, - "required": ["customerName", "productIds"] - } - } + "type": "assistant", + "assistantName": "Specialist", + "description": "Handle billing questions" + } + ], + "function": { + "name": "handoff_to_specialist", + "parameters": { + "type": "object", + "properties": { + "destination": { + "type": "string", + "enum": ["Specialist"] + }, + "customerIntent": { + "type": "string", + "description": "The caller's reason for contacting billing" } - ] + }, + "required": ["destination", "customerIntent"] } - ] + } } ``` -### Variable access patterns - -Once extracted, variables are accessible using Liquid template syntax (`{{variableName}}`). The access pattern depends on the schema structure: +The destination can use `{{customerIntent}}` in its prompt. `destination` selects the target and is not saved as a variable. For fixed destinations, Vapi derives the allowed destination values from the configured destinations. Dynamic routing can leave `destination` optional so the webhook can resolve it. Since the model supplies custom arguments, verify critical IDs against a trusted source. -| Schema type | Access pattern | Example | -|-------------|---------------|---------| -| Simple property | `{{propertyName}}` | `{{customerName}}` | -| Nested object | `{{object.property}}` | `{{name.first}}`, `{{name.last}}` | -| Array item | `{{array[index]}}` | `{{zipCodes[0]}}`, `{{zipCodes[1]}}` | -| Array of objects | `{{array[index].property}}` | `{{people[0].name}}`, `{{people[0].age}}` | -| Nested array | `{{array[index].nestedArray[index]}}` | `{{people[0].zipCodes[1]}}` | +### Destination schema extraction - -Top-level object properties are extracted as direct global variables. For example, a schema with properties `name` and `age` produces `{{name}}` and `{{age}}` -- not `{{root.name}}`. - - -### Variable aliases - -Use **aliases** to create additional variables derived from extracted values. Aliases support Liquid template syntax for transformations and compositions. +Use `variableExtractionPlan.schema` on the destination when you need a separate request to extract information from the outgoing assistant's currently available conversation: ```json { + "type": "assistant", + "assistantName": "Specialist", "variableExtractionPlan": { "schema": { "type": "object", "properties": { - "firstName": { - "type": "string", - "description": "Customer's first name" - }, - "lastName": { + "customerName": { "type": "string", - "description": "Customer's last name" - }, - "company": { - "type": "string", - "description": "Customer's company name" + "description": "Name the caller gave during the conversation" } - } - }, - "aliases": [ - { - "key": "fullName", - "value": "{{firstName}} {{lastName}}" - }, - { - "key": "greeting", - "value": "Hello {{firstName}}, welcome to {{company}}!" }, - { - "key": "customerCity", - "value": "{{addresses[0].city}}" - } - ] + "required": ["customerName"] + } } } ``` -Each alias creates a new variable accessible as `{{key}}` during the call and stored in `call.artifact.variableValues` after the call. Alias keys must start with a letter and contain only letters, numbers, or underscores (max 40 characters). +The destination can read the top-level property as `{{customerName}}`. A nested object can be read with a path such as `{{shippingAddress.city}}`. An earlier history filter can limit what the extraction request sees; it does not recover messages removed from the outgoing assistant's context. -### Extraction via `tool.function` +Extraction is best effort. Without a `schema`, the plan adds nothing. If the request fails or returns a non-object, the handoff continues without new extracted values. A required schema property is not guaranteed to be available in the destination prompt. -You can also extract variables through the LLM tool call parameters (in addition to sending these parameters to your server in a `handoff-destination-request` for dynamic handoffs). Include the **destination** parameter with the assistant names or IDs in `enum` -- Vapi uses this to determine where to hand off the call. The `destination` parameter itself is not extracted as a variable. Add `destination` and all other required variables to the schema's `required` array. +Use [tool-result aliases](/tools/static-variables-and-aliases/extract-and-chain) on the tool that produces a response when you need exact fields from that response. A handoff destination's `variableExtractionPlan` uses its **`schema`** for conversation extraction; aliases there are not applied by the current handoff path. -```json -{ - "tools": [ - { - "type": "handoff", - "destinations": [ - { - "type": "assistant", - "assistantName": "order-processing-assistant", - "description": "customer is ready to place an order" - } - ], - "function": { - "name": "handoff_to_order_processing_assistant", - "parameters": { - "type": "object", - "properties": { - "destination": { - "type": "string", - "description": "The destination to handoff the call to.", - "enum": ["order-processing-assistant"] - }, - "customerName": { - "type": "string", - "description": "Full name of the customer" - }, - "email": { - "type": "string", - "format": "email", - "description": "Customer's email address" - }, - "productIds": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of product IDs customer wants to order" - }, - "shippingAddress": { - "type": "object", - "properties": { - "street": { "type": "string" }, - "city": { "type": "string" }, - "state": { "type": "string" }, - "zipCode": { "type": "string" } - } - } - }, - "required": ["destination", "customerName", "email"] - } - } - } - ] -} -``` +### Reuse values after another handoff + +Suppose assistant A passes `customerIntent` as a handoff argument to B. B extracts `customerName`, then hands off to C with `contextEngineeringPlan.type` set to `none`. C receives no earlier conversation messages, but can still use `{{customerIntent}}` and `{{customerName}}` from the shared variable bag. If B sets `customerIntent` again, its later value replaces A's value. + +## Configuration paths and verification + +The history and variable rules above apply to a completed assistant or squad handoff. Check how the handoff is configured: + +| Path | What to check | +| --- | --- | +| Saved squad, API or Dashboard | Use a handoff tool on the outgoing assistant, or configure a squad member's Handoff Tools. For custom `function.parameters`, configure the tool itself; the squad member Handoff Tools setting creates a tool without those parameters. | +| Saved handoff tool in the Dashboard | The Tools page exposes function parameters for the handoff tool. Save the tool and attach it to the outgoing assistant. | +| Standalone assistant in a live call | A destination can identify a saved assistant by `assistantId`, or provide an inline assistant. Verify the destination resolves in the call. | +| `/chat` API | Assistant destinations require a squad context with more than one assistant. A standalone assistant's handoff tool call can return a handoff result without activating its destination. | +| Dynamic destination | A `handoff-destination-request` webhook chooses the destination at runtime. Its returned destination can include context and extraction plans. See [Dynamic handoffs](/squads/handoff/destinations#dynamic-handoffs). | + +To confirm a handoff, inspect the call or chat transcript for the outgoing handoff tool call and the next assistant's response. For calls, use `call.id` to correlate records and inspect `artifact.variableValues` for values retained in the call. The active conversation used by analysis may differ from the artifact after context filtering; set [`artifactPlan.fullMessageHistoryEnabled`](/assistants/call-recording) if you need the full cross-assistant message history in the call artifact. diff --git a/fern/squads/passing-data-between-assistants.mdx b/fern/squads/passing-data-between-assistants.mdx index 94c099f9e..ab1051241 100644 --- a/fern/squads/passing-data-between-assistants.mdx +++ b/fern/squads/passing-data-between-assistants.mdx @@ -1,188 +1,48 @@ --- title: Passing data between assistants -subtitle: Three approaches for forwarding context to the next assistant in a squad — when to use each, and what each one costs +subtitle: Choose how values reach the next assistant in a handoff slug: squads/passing-data-between-assistants -description: Forward context to the next assistant in a Vapi squad using three handoff mechanisms, with a decision guide on their latency, accuracy, and trade-offs. +description: Compare handoff arguments, destination extraction, tool-result aliases, and Liquid variables for passing data between assistants. --- -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. +Assistants in a call share variable values across completed handoffs. A destination can read those values even when its `contextEngineeringPlan` excludes earlier conversation messages. Start with the value's source: -This page is a decision guide. For end-to-end configuration of the handoff itself, see the [Handoff tool](/squads/handoff) page. +| If the value comes from... | Use... | What happens | +| --- | --- | --- | +| The handoff decision, such as classified intent | [Handoff arguments](#approach-1-handoff-arguments) | The outgoing model fills custom `function.parameters` in the handoff tool call. Every argument except `destination` is saved as a variable. | +| Conversation content that needs a dedicated extraction request | [Destination schema extraction](#approach-2-variable-extraction) | Vapi extracts an object from the outgoing assistant's currently available conversation and saves its properties. | +| A previous tool's JSON response | [Tool-result aliases](/tools/static-variables-and-aliases/extract-and-chain) | Aliases on the result-producing tool select fields to save. The raw response is not automatically saved as variables. | +| A standard call value or a value already saved | [Liquid in the destination prompt](#approach-3-liquid-templating) | The destination reads it with `{{variableName}}`; Liquid does not create a value. | -## The three approaches at a glance - -| Approach | Where the value comes from | LLM involved? | Latency | Hallucination risk | Best for | -| -------- | -------------------------- | ------------- | ------- | ------------------ | -------- | -| **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 | -| **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 | -| **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 | +For the history modes, value precedence, configuration paths, and a two-hop example, see [Pass context and variables](/squads/handoff/context-and-variables). ## Approach 1: Handoff arguments -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. - - -**Availability today:** - -- **API:** Fully supported. Send the JSON below via `POST /tool` or as part of your assistant's `model.tools[]` via `POST /assistant` / `PATCH /assistant`. -- **Dashboard — Tools page:** UX for defining `function.parameters` on a handoff tool is shipping soon. Use the API in the meantime. -- **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. - - - - -```json -{ - "type": "handoff", - "function": { - "name": "handoff_to_specialist", - "description": "Hand off to the specialist when the customer is ready", - "parameters": { - "type": "object", - "required": ["destination", "customerIntent", "customerSentiment"], - "properties": { - "destination": { - "type": "string", - "enum": ["specialist"] - }, - "customerIntent": { - "type": "string", - "enum": ["new-customer", "existing-customer", "billing-issue"], - "description": "What the customer is calling about" - }, - "customerSentiment": { - "type": "string", - "enum": ["positive", "neutral", "frustrated"], - "description": "Caller's overall sentiment" - } - } - } - }, - "destinations": [ - { - "type": "assistant", - "assistantName": "Specialist" - } - ] -} -``` - -The next assistant receives `customerIntent` and `customerSentiment` in the variable bag and can reference them as `{{customerIntent}}` / `{{customerSentiment}}` in its prompts. - -**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. - -**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. - -## Approach 2: Variable extraction (`variableExtractionPlan.schema`) - -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. - -```json -{ - "type": "assistant", - "assistantName": "Scheduler", - "variableExtractionPlan": { - "schema": { - "type": "object", - "required": ["preferredDate", "preferredTime"], - "properties": { - "preferredDate": { - "type": "string", - "description": "The date the caller asked to schedule for, in YYYY-MM-DD format" - }, - "preferredTime": { - "type": "string", - "description": "The time of day the caller asked for, in 24-hour HH:MM format" - } - } - } - } -} -``` +Define custom `function.parameters` on the handoff tool when the outgoing assistant should supply a value as it routes the caller. The model fills these arguments in the same tool call that initiates the handoff. For example, a `customerIntent` argument becomes `{{customerIntent}}` for the destination; `destination` only selects where the handoff goes. -**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`. +Configure custom parameters on a saved or inline handoff tool through the API or the Dashboard Tools page. A squad member's **Handoff Tools** destination setting creates a handoff tool without custom `function.parameters`, so configure the tool itself when you need arguments. See the [handoff argument example](/squads/handoff/context-and-variables#handoff-arguments). -**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. +The model produces these values. For an ID from a database or another trusted system, extract the exact field from the result-producing tool with an alias instead of asking the model to repeat it. -For full configuration details — multiple destinations, dynamic handoffs, context engineering — see the [Variable extraction section of the Handoff tool page](/squads/handoff/context-and-variables#variable-extraction). +## Approach 2: Variable extraction -## Approach 3: Liquid templating in the destination's prompt +Put `variableExtractionPlan.schema` on the handoff destination when a value must be assembled from conversation content. Vapi makes a separate extraction request against the outgoing assistant's available conversation and adds the returned top-level properties before the destination starts. -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. +This path adds a model request at handoff time. Extraction is best effort: if the request fails or returns a non-object, the handoff continues without new extracted values. An earlier history filter can limit what the extraction request sees. See the [destination schema example](/squads/handoff/context-and-variables#destination-schema-extraction). -```text -You are the scheduling specialist. The caller is {{customer.name}}, calling -from {{customer.number}}. Their patient ID is {{patientId}} (looked up earlier -this call). They want a {{preferredAppointmentType}} appointment. - -Today is {{currentDateTime}}. -``` +## Approach 3: Liquid templating -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). - -**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. - -**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. - - -**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. - - -## Decision flowchart +Use Liquid when the value is already available. This includes supported [standard call and time variables](/assistants/dynamic-variables), custom values supplied at call start, handoff arguments, earlier schema extraction, and values selected from tool results with aliases. ```text -What do you want the next assistant to know? -│ -├─ "Something the model just heard / classified / summarized" -│ └─→ Approach 1: Handoff arguments -│ Zero added latency, model fills inline. -│ -├─ "Something the user explicitly said and I want a dedicated, schema-validated extraction" -│ └─→ Approach 2: variableExtractionPlan.schema -│ Adds an LLM round-trip, but you get structured output and a focused -│ extraction prompt. -│ -└─ "Something I already have — call data, prior tool result, prior extraction" - └─→ Approach 3: Reference it via Liquid in the destination's prompt - No extra cost. Use {{customer.number}}, {{patientId}}, etc. directly. +You are the scheduling specialist. The caller's number is {{customer.number}}. +Their patient ID is {{patientId}} and their requested date is {{preferredDate}}. ``` -## Common patterns - -### Pattern: "Forward an extracted ID after a database lookup" - -A `lookupPatient` tool returned `{patientId: "p_42", dob: "1990-01-15"}` on assistant A. Assistant B needs `patientId`. - -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. - -### Pattern: "Categorize what the caller wants and route on it" - -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. - -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}}`. - -### Pattern: "Pull a structured booking request out of free-form speech" - -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. - -Use **Approach 2** — `variableExtractionPlan.schema` with the destination. The dedicated extraction prompt + schema validation catches the structure better than inline arguments. - -### Pattern: "Mix and match" - -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. - -## What if extraction fails? - -Vapi's handoff path is failure-isolated: - -- An empty `variableExtractionPlan` (`{}`) is a graceful no-op — the handoff proceeds without extraction. -- 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. -- 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. +The prompt only reads these values. If `patientId` came from a `lookupPatient` response, configure an alias on that tool to save the exact response field first. Check that a value exists before depending on it in the destination prompt. -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). +## Combining approaches -## Next steps +An A → B → C flow can use more than one method. A can hand off to B with a model-generated `customerIntent` argument. B can run a lookup tool with an alias that saves `patientId`, then hand off to C with `contextEngineeringPlan.type` set to `none`. C gets a fresh message history but can still render `{{customerIntent}}` and `{{patientId}}` from the shared variable bag. Later values with the same key replace earlier ones. -- [Handoff tool](/squads/handoff) — full configuration reference for the handoff tool itself. -- [Static variables and aliases](/tools/static-variables-and-aliases) — how the variable bag is built and what's available in scope. -- [Dynamic variables](/assistants/dynamic-variables) — set initial variables when starting a call. +For destination setup, see the [Handoff tool](/squads/handoff) and [Configure handoff destinations](/squads/handoff/destinations). diff --git a/fern/tools/static-variables-and-aliases/extract-and-chain.mdx b/fern/tools/static-variables-and-aliases/extract-and-chain.mdx index ce7535746..df1badca2 100644 --- a/fern/tools/static-variables-and-aliases/extract-and-chain.mdx +++ b/fern/tools/static-variables-and-aliases/extract-and-chain.mdx @@ -185,7 +185,7 @@ Static `parameters` is not a field on the Handoff tool itself -- handoff doesn't 2. **Server-trusted derived data flows forward via the variable bag.** Aliases extracted by an earlier assistant's `variableExtractionPlan` (from a server-trusted source -- for example, an `apiRequest` keyed on `{{ customer.number }}`) persist across handoffs and remain referenceable as Liquid variables in the next assistant's tools. 3. **Static handoff-time injection via `destination.assistantOverrides.variableValues`.** Defined statically in the handoff configuration, merged into the variable bag at handoff time, bypasses the LLM entirely. Use this for per-destination config the next assistant should know about (`{ "tier": "premium" }`, `{ "slaWindowSeconds": 30 }`). -For full coverage of the three approaches, when to choose each, and the latency/accuracy tradeoffs, see [Passing data between assistants](/squads/passing-data-between-assistants). +For a source-based comparison of handoff arguments, destination extraction, tool-result aliases, and Liquid values, see [Passing data between assistants](/squads/passing-data-between-assistants). **Threat-model note for security-sensitive values.** The squads guide's *Approach 1: Handoff arguments* (using `function.parameters` on the Handoff tool) is correct for **LLM-derived** values like classifications, summaries, sentiment, intent. It is **not** a security boundary -- the model fills those args, and prompt injection can corrupt them. For **signaling-derived** trusted values like the verified caller-ID, only the call-level Liquid variables (Approach 3 in the squads guide) keep the LLM out of the chain.