Skip to content

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[].squad on 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.CreateSquadDto whose 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[].assistantId pointing at a sibling) is closed. There's no ID to point at.
  • Avoca's own HandoffTool works around the lack of persistent IDs by using destinations[].assistantName, which resolves within the current squad's members[]. Shop A's compiled squad contains only Shop A's sub-agents. Shop B is not a member of that squad, so assistantName resolution can't reach it.
  • The remaining destination shapes Vapi's schema accepts (destinations[].assistant for an inline assistant config, destinations[].squad for 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-request webhook 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

DimensionPattern A (eager)Pattern B (lazy)
When sibling compilesSource-call-startAt handoff invocation
Source-call-start payload sizeLarger (all siblings inlined)Smaller (placeholders only)
Wasted compute on unused siblingsYes (compile-but-never-invoke)No
Handoff-moment latencyNone (already inlined)Compile latency on the wait
New webhook to operateNoYes (handoff-destination-request)
Data freshnessFrozen at source-call-startFresh at handoff time
Implementation effortLowerHigher

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

  1. 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[].squad shape from Vapi's perspective; the dispatcher swap is straightforward.

  2. 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.json precedent.
    • γ. Implicit via enterprise membership (a parent_org_id on 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.

  3. contextEngineeringPlan choice. Today's intra-squad handoff hard-codes { type: 'all' }. Cross-Blueprint may want a different default (e.g. userAndAssistantMessages to 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.

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

  5. 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-request webhook 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:

  1. Construct a Vapi.CreateHandoffToolDto by hand with a single destinations[].squad inline destination (the same CreateSquadDto shape MultiAgent.getVapiAssistantReq() produces).
  2. Attach to a test assistant via Hamming.
  3. Drive a call past the handoff trigger.
  4. 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).