Skip to content

Tutorial — cross-app lead handoff

This tutorial builds two real apps — demo-marketing and demo-sales — that exercise every cross-app collaboration surface end to end:

  • ctx.events.emit + approved wire → cross-app pub/sub
  • @app.on_event subscriber handler in the receiving app
  • ctx.team.delegate → intra-app master/researcher hand-off
  • ctx.apps.invoke + bilateral grant → cross-app RPC for enrichment
  • ctx.rooms.post → human + agents in one shared room
  • ctx.commitments.add → due-dated promises that surface in the inbox
  • heartbeats: → daily review that wakes only when there’s work

It’s the canonical “agents talking to agents” demo. The full source lives in examples/demo-marketing and examples/demo-sales.

Inbound email "we want pricing for 200 seats"
demo-marketing.on_inbound
├─ master agent: "is this a lead?" → "LEAD"
├─ ctx.team.delegate("researcher", ...) → score=85 reason=...
├─ ctx.events.emit("lead_qualified", {from, score, reason, snippet}) ╮
├─ ctx.rooms.post(shared_room, "📥 New qualified lead: ...") │
└─ ctx.commitments.add("Send marketing kit to ...", due=+24h) │
│ wire fires
demo-sales.on_event("lead_qualified")
├─ ctx.rooms.post(shared_room, "✅ acknowledged ...")
├─ ctx.commitments.add("First touch: ...", due=+4h)
└─ ctx.apps.invoke("demo-marketing", "researcher", ...) ── grant fires ──→
(back to Marketing's
researcher for an
enrichment summary)
└─ ctx.rooms.post(shared_room, "📋 Marketing enrichment: ...")
Tomorrow 09:00 UTC
demo-sales.heartbeat[morning_review]
└─ pre-filter on _internal_app_records / commitments → wake only if pending exists
└─ BDR agent surfaces stale commitments back into the room

Marketing — emits + master/researcher team

Section titled “Marketing — emits + master/researcher team”
# linkworld.app.yaml (demo-marketing)
agents:
- id: master
is_default: true
team: [researcher] # master may delegate to researcher
system_prompt: ...
- id: researcher
system_prompt: ...
emits:
- lead_qualified
# linkworld.app.yaml (demo-sales)
agents:
- id: bdr
is_default: true
system_prompt: ...
heartbeats:
- slug: morning_review
skill: _internal_app_records
tool: query
params: { kind: commitments, state: pending }
schedule_type: daily
time_of_day: "09:00"
subscribes_to:
- emitter_app: demo-marketing
event_name: lead_qualified
target_agent: bdr

The manifest declares the subscription; the code handler is what actually runs. @app.on_event is the decorator for exactly this — the subscription wire targets bdr, and the SDK routes the dispatched event to the matching decorator:

from linkworld_sdk import App, AgentCallError, AppEvent, ToolCallError
app = App.from_manifest("linkworld.app.yaml")
@app.on_event("lead_qualified")
async def on_lead(ctx, event: AppEvent) -> None:
payload = event.payload or {}
sender = payload.get("from", "unknown")
score = payload.get("score", 0)
# Acknowledge in the shared room
room_id = await ctx.secrets.get("DEMO_LEAD_HANDOFF_ROOM_ID")
if room_id:
await ctx.rooms.post(
room_id,
f"✅ acknowledged — picking up {sender} (score {score}/100)",
from_agent="bdr",
)
# Self-commitment
await ctx.commitments.add(
f"First touch: {sender}",
f"Lead handed off from {event.emitter_app_id}, score {score}.",
from_agent="bdr",
due_at="+4h",
)
# Cross-app: ask Marketing for enrichment (needs grant)
try:
enrichment = await ctx.apps.invoke(
event.emitter_app_id, "researcher",
f"3-bullet enrichment summary for {sender} (score {score}).",
max_tokens=400,
)
if room_id and enrichment.text:
await ctx.rooms.post(
room_id,
f"📋 Marketing enrichment for {sender}:\n{enrichment.text}",
from_agent="bdr",
)
except AgentCallError as exc:
ctx.logger.info("no grant yet: %s", exc)
Terminal window
cd examples/demo-marketing
docker build -t ghcr.io/<you>/demo-marketing:0.1.0 .
docker push ghcr.io/<you>/demo-marketing:0.1.0
linkworld deploy
cd ../demo-sales
docker build -t ghcr.io/<you>/demo-sales:0.1.0 .
docker push ghcr.io/<you>/demo-sales:0.1.0
linkworld deploy

Use the App Marketplace in Workspace Control → Apps. Click “Install” on each. Granted scopes default to whatever the manifest declares.

Workspace Control → Wires & Grants:

  • Approve the wire demo-marketing.lead_qualified → demo-sales.bdr (both sides need approval — admins click both buttons)
  • Approve the grant demo-sales → demo-marketing/researcher (same — bilateral consent before any cross-app call works)

Workspace Control → Rooms → New room.

Name it something like Lead Hand-off. Add both agents as members:

  • demo-marketing.master
  • demo-sales.bdr

Copy the room’s UUID from the URL.

Workspace Control → app detail page → Secrets.

For both demo-marketing and demo-sales, set:

key: DEMO_LEAD_HANDOFF_ROOM_ID
value: <the room UUID from step 4>

Either:

  • Send a chat message to your tenant agent that looks like a lead, e.g. “We need pricing for 200 seats and a demo next week.”
  • Or send an actual email through whatever inbound channel you wired.

Marketing’s on_inbound should fire, classify it, qualify it, emit, post in the room, and create the kit-send commitment. Within seconds Sales’ @app.on_event fires, posts the acknowledgement, creates the first-touch commitment, and (if the grant is approved) calls back into Marketing for an enrichment summary that lands in the same room.

WhereWhat you’ll see
Inbox”Send marketing kit to …” (Marketing) + “First touch: …” (Sales)
Rooms → Lead Hand-off3 messages: 📥 from Marketing, ✅ from Sales, 📋 enrichment from Sales
Audit logthe cross-app invoke + emit + room posts + commitment creates
Health → Heartbeatsdemo-sales/morning_review row, last_run shows up after 09:00 UTC
KPI Cockpitnew “qualified lead” rows pile up
Stewardask “what happened with the lead from [email protected]?” — it’ll narrate

Both apps ship linkworld.eval.yaml files exercising every branch without LLMs. Run from each app’s directory:

Terminal window
linkworld eval # 4 scenarios per app, all green
linkworld eval --filter event # only the on_event scenarios

For the live playground (chat UI in browser):

Terminal window
linkworld chat # mock agent responses from linkworld.dev.json
linkworld chat --live # uses real Anthropic via ANTHROPIC_API_KEY

This demo isn’t just a teaching example — it’s the canonical end-to-end test for the cross-app collaboration stack. When you run it, you exercise:

  • Container provisioning + cold-start for two apps simultaneously
  • Inbound fan-out → SDK on_inbound handler dispatch
  • LLM agent invocation (master) + delegation (researcher)
  • Event emit → bilateral wire approval check → fan-out
  • Subscriber container resolution + dispatch_event to app_event:*
  • SDK runtime → @app.on_event handler routing
  • ctx.rooms.post + member resolution + audit
  • ctx.commitments.add + delivery_target dispatch
  • Cross-app invoke + bilateral grant check + chain-depth header
  • Heartbeat scheduler tick + pre-filter + invoke when needed

If any of those break, you find out here — not in front of a real customer.