Appearance
Cross-Blueprint handoff
One-line summary. A new Blueprint primitive that lets a caller's live call be handed off mid-conversation to a different Blueprint (e.g. a sibling shop within the same enterprise). Source Blueprint's dispatcher reads a routing config naming allowed sibling Blueprints, the sibling is compiled to a transient squad, and the squad is inlined as
destinations[].squadon a new handoff tool variant. Vapi swaps the active assistant onto the sibling without dropping the call. EAS (Ponderosa caller booking at Stonebridge) is the motivating use case; the primitive is general.
Why this is a proposal, not a PR
This adds a new tool primitive to Blueprint, a new dispatcher path for compiling sibling Blueprints, an authoring surface for cross-Blueprint routing, and a context-engineering decision Blueprint doesn't expose today. Several design questions (eager vs lazy compilation, where the routing config lives, what context to forward) need acceptance before implementation. The page exists to anchor that conversation.
Problem statement
A caller dials Shop A for booking. During the conversation it becomes clear the appointment is actually for Shop B (an enterprise sibling: same parent org, separate Blueprint, separate tenant). Today, the agent has to ask the caller to hang up and redial Shop B. Hangup-then-redial is a real abandonment surface; many callers don't make the second call.
The motivating instance is EAS: Ponderosa and Stonebridge are siblings within the EAS enterprise, each with its own Blueprint (own prompt, own CRM tenant, own transfer destinations, own voice-assistant configuration). The primitive should generalize to any multi-shop enterprise on Blueprint, not just EAS.
Why existing Blueprint doesn't solve this
A reasonable first response from anyone who built Blueprint: "Blueprint already composes agents. The React Flow editor connects sub-agents with edges and handoffs. Why do we need anything new?"
The answer is that Blueprint's existing composition primitives operate within one Blueprint:
- A Blueprint is the unit of one shop's behavior. Its sub-agents share a system prompt baseline, a tool set, a CRM tenant, a voice-assistant configuration, transfer destinations, and identity context.
- The React Flow graph connects those sub-agents. The compiled output is a single
Vapi.CreateSquadDtowhose members are the sub-agents of that one Blueprint. - A sibling shop lives in a different Blueprint, which compiles to a different
CreateSquadDto. Its sub-agents are not visible to the first Blueprint's sub-agents at compile time.
You could in principle collapse two shops into one Blueprint as separate sub-agents. The cost is real:
- Whose config does the embedded sibling sub-agent use? Sub-agents inside a Blueprint share config. The sibling shop has different transfer destinations, a different prompt baseline, a different CRM tenant. Forcing them into one Blueprint either picks a winner (silently wrong) or invents a per-sub-agent config override system Blueprint doesn't have today.
- N^2 duplication. If shop A wants to reach B and C, and B wants to reach A and C, every Blueprint embeds copies of the others. Editing Stonebridge's prompt means touching every Blueprint that referenced Stonebridge.
- Tenant boundary erosion. Each Blueprint today corresponds to one tenant's authoring scope. Cross-Blueprint composition by collapsing breaks the unit-of-isolation the rest of Blueprint depends on.
The cross-Blueprint primitive is the layer above sub-agent composition. It lets one Blueprint, compiled in isolation, hand off to another Blueprint, also compiled in isolation, without ever collapsing them into a single authoring unit.
The relevant baseline: how this would work in Vapi-direct (no Blueprint)
To see what's missing, picture the same problem solved outside of Blueprint. If each EAS shop were authored as its own persistent Vapi assistant via Prompt Builder (the standard Vapi-direct path), each shop would have a stable assistantId. Vapi's handoff tool would be configured per assistant with destinations[].assistantId pointing at the sibling assistants. Vapi would swap calls between persistent assistants at runtime. This is a normal, supported Vapi pattern.
System view: Vapi-direct cross-shop handoff (hypothetical, not Avoca's setup)
The pattern is straightforward: persistent assistants, each with an ID, referenced by ID in handoff destinations. Vapi handles the swap because it already knows the destination assistant exists.
This is not how EAS is set up
EAS shops are not authored directly in Vapi's Prompt Builder. They're authored as Blueprints, and Blueprint's compilation model removes the persistent-assistantId foundation this baseline depends on. Read on for why.
Why Blueprint can't reach this baseline today
Blueprint doesn't author persistent assistants. Each shop's Blueprint compiles transiently, at the moment a call lands on that shop's number, into a Vapi.CreateSquadDto that Vapi runs for the duration of that one call and discards when the call ends. The compiled assistant has no stable assistantId; Vapi has no prior knowledge of it; nothing about Shop B's compiled output exists in Vapi's persistent storage that Shop A's call could reference.
Consequences for cross-shop handoff:
- The Vapi-direct path (
destinations[].assistantIdpointing at a sibling) is closed. There's no ID to point at. - Avoca's own
HandoffToolworks around the lack of persistent IDs by usingdestinations[].assistantName, which resolves within the current squad'smembers[]. Shop A's compiled squad contains only Shop A's sub-agents. Shop B is not a member of that squad, soassistantNameresolution can't reach it. - The remaining destination shapes Vapi's schema accepts (
destinations[].assistantfor an inline assistant config,destinations[].squadfor an inline squad config) would in principle let a transient sibling Blueprint be inlined as a destination. No Avoca code path emits either shape. Verified in D2 gap verification.
So cross-Blueprint handoff is blocked not by Vapi (the schema accepts inline transient destinations) and not by intent (the use case is real and recurring), but by the absence of Avoca code that emits inline destinations for sibling Blueprints' compiled squads.
What happens today when cross-Blueprint is needed
The dead end isn't "Blueprint tried and failed." It's "Blueprint has no tool definition to offer the LLM." The caller's only path forward is hanging up and dialing Shop B's number themselves.
Proposed architecture
The proposed primitive: a new handoff tool variant (working name CrossBlueprintHandoffTool) that, instead of emitting destinations[].assistantName (intra-squad reference), emits destinations[].squad: <inline CreateSquadDto> for each allowlisted sibling Blueprint. Vapi's schema accepts this shape (verified at the spec layer; confirmed unused in production by D2 gap verification). When the LLM calls the new handoff tool, Vapi swaps the active assistant onto the inlined sibling squad with no callback to Avoca for the swap itself.
Two open design choices govern when the sibling squad is compiled:
- Pattern A: eager. Source-call dispatcher compiles every allowlisted sibling at source-call-start and inlines all of them in the source's handoff tool payload.
- Pattern B: lazy. Source-call dispatcher only references the siblings by name; Vapi's
handoff-destination-requestwebhook fires at handoff time, Avoca compiles the chosen sibling on demand and returns it.
System view (Pattern A diagrammed; Pattern B differs only in compile timing)
The routing config (file or DB; see open questions) declares allowed siblings per Blueprint. The dispatcher reads it, compiles each allowed sibling to a transient squad, and inlines them as destinations on the new handoff tool. Blueprint authors don't touch routing; ops/FDE configures it. Same pattern shape as shop-routing.json for transfer-call time-routing.
Sequence view: Pattern A (eager compilation at source-call-start)
Trade-off: every Shop A call pre-compiles all allowlisted siblings, paying their compile cost even when handoff never fires. Payload size at source-call-start grows linearly with allowlist length.
Sequence view: Pattern B (lazy compilation via dynamic-handoff webhook)
Trade-off: compile cost moves from "every source call" to "every handoff invocation," which is a smaller multiplier. But the compile latency happens while the caller is waiting for the handoff to land, not at the moment of dial. Also requires implementing and operating the handoff-destination-request webhook handler.
Pattern comparison
| Dimension | Pattern A (eager) | Pattern B (lazy) |
|---|---|---|
| When sibling compiles | Source-call-start | At handoff invocation |
| Source-call-start payload size | Larger (all siblings inlined) | Smaller (placeholders only) |
| Wasted compute on unused siblings | Yes (compile-but-never-invoke) | No |
| Handoff-moment latency | None (already inlined) | Compile latency on the wait |
| New webhook to operate | No | Yes (handoff-destination-request) |
| Data freshness | Frozen at source-call-start | Fresh at handoff time |
| Implementation effort | Lower | Higher |
Pattern B dominates on compute and data freshness; Pattern A is simpler to implement and avoids adding a new webhook surface. The choice is the central question for the ADR.
Open design questions
Eager (Pattern A) vs lazy (Pattern B) compilation. The trade-off above. Recommend opening with Pattern A for V1 (simpler, no new webhook), move to Pattern B if compile-cost-at-call-start becomes measurable. Both produce the same
destinations[].squadshape from Vapi's perspective; the dispatcher swap is straightforward.Authoring surface for the allowlist. Three shapes considered:
- α. Per-Blueprint configuration (in the Blueprint's React Flow / JSON). Authors declare allowed siblings inline. Verbose, scales poorly, breaks tenant abstraction (one Blueprint shouldn't need to name another).
- β. Centralized routing config (file-in-repo or DB). One source of truth; ops/FDE owns it. Matches
shop-routing.jsonprecedent. - γ. Implicit via enterprise membership (a
parent_org_idon each Blueprint, allowlist = "all Blueprints sharing my parent"). Cleanest but inflexible (no per-shop carve-outs, no asymmetric allowlists).
Recommend β for V1, with γ as a future direction once enterprise membership is modeled.
contextEngineeringPlanchoice. Today's intra-squad handoff hard-codes{ type: 'all' }. Cross-Blueprint may want a different default (e.g.userAndAssistantMessagesto avoid forwarding the source Blueprint's tool-result history to a sibling that has no use for it; token-cost reduction). Worth making per-Edge configurable rather than committing to one default.Allowlist enforcement layer. Where the routing-config check actually runs. Options: at tool-build time only (cheap, but a future code change could bypass it), at dispatch time AND inside the runtime path (defense in depth, slightly higher cost). Recommend defense in depth.
Context variable re-injection. Call-scoped
variableValues(caller-ID, customer context, Blueprint-resolved metadata) are preserved through intra-squad handoff. For cross-Blueprint, the receiving Blueprint may resolve different variables from the same caller identity. Question: do we re-run the source Blueprint's variable resolution against the sibling Blueprint's compile context, or forward what's there as-is?
Risks and unknowns
- Inline transient destinations are spec-supported but not exercised at Avoca. Vapi's schema accepts
destinations[].squad: <inline CreateSquadDto>(verified in research). No code path in avoca-next produces them today (verified in D2). The first production deployment will be the first Avoca call with this shape; failure modes may surface that aren't visible in the schema. - Payload size at source-call-start (Pattern A). Several inlined squads per call. Vapi may have payload limits we haven't hit. Phase 0 verification should establish the practical limit.
- Handoff-moment latency (Pattern B). Sibling Blueprint compile is non-trivial (DB reads, Liquid rendering, tool list assembly). Adding it to the handoff wait could regress caller experience. Measurement needed before committing to Pattern B.
handoff-destination-requestwebhook is a new external surface. Operationally: authentication, retry behavior, error handling, observability all need to match the existing webhook stack. Adds maintenance burden.
Phase 0: verification before building
Before writing the cross-Blueprint primitive, run a probe that proves the runtime path works with Vapi today:
- Construct a
Vapi.CreateHandoffToolDtoby hand with a singledestinations[].squadinline destination (the sameCreateSquadDtoshapeMultiAgent.getVapiAssistantReq()produces). - Attach to a test assistant via Hamming.
- Drive a call past the handoff trigger.
- Verify the swap completes and the receiving inline squad is active.
This catches a category of issues no amount of static analysis can rule out (e.g., Vapi imposing a runtime constraint not in the public schema, payload size limits, context-engineering behavior differences for inline vs by-name destinations).
Related canon and plans
tool-calls/index.md— canonical reference for how tool calls work in Avoca, including the "Handoff: how it works today" section that establishes the baseline this proposal extends.cross-shop-routing-mechanism-research/research.md— Vapi platform feasibility research. Conclusion: Vapi accepts inline destinations; the gap is in Avoca code.cross-blueprint-handoff/brief.md— L3 plan brief for the implementation work.cross-blueprint-handoff/discovery-d2-gap-verification.md— code-side confirmation that no Avoca path emits inline destinations today.cross-shop-availability-and-commit-tool.md— sibling work: cross-shop availability fan-out and the commit-tool pattern. Cross-Blueprint handoff is a natural follow-up but is functionally independent.