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_eventsubscriber handler in the receiving appctx.team.delegate→ intra-app master/researcher hand-offctx.apps.invoke+ bilateral grant → cross-app RPC for enrichmentctx.rooms.post→ human + agents in one shared roomctx.commitments.add→ due-dated promises that surface in the inboxheartbeats:→ 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.
The scenario
Section titled “The scenario”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 roomManifest highlights
Section titled “Manifest highlights”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_qualifiedSales — subscribes + heartbeat
Section titled “Sales — subscribes + heartbeat”# 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: bdrSubscriber-side handler
Section titled “Subscriber-side handler”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)Walkthrough on a dev tenant
Section titled “Walkthrough on a dev tenant”1. Build + deploy both apps
Section titled “1. Build + deploy both apps”cd examples/demo-marketingdocker build -t ghcr.io/<you>/demo-marketing:0.1.0 .docker push ghcr.io/<you>/demo-marketing:0.1.0linkworld deploy
cd ../demo-salesdocker build -t ghcr.io/<you>/demo-sales:0.1.0 .docker push ghcr.io/<you>/demo-sales:0.1.0linkworld deploy2. Install both on your tenant
Section titled “2. Install both on your tenant”Use the App Marketplace in Workspace Control → Apps. Click “Install” on each. Granted scopes default to whatever the manifest declares.
3. Set up the wire + grant
Section titled “3. Set up the wire + grant”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)
4. Create the shared room
Section titled “4. Create the shared room”Workspace Control → Rooms → New room.
Name it something like Lead Hand-off. Add both agents as members:
demo-marketing.masterdemo-sales.bdr
Copy the room’s UUID from the URL.
5. Wire the room into both apps
Section titled “5. Wire the room into both apps”Workspace Control → app detail page → Secrets.
For both demo-marketing and demo-sales, set:
key: DEMO_LEAD_HANDOFF_ROOM_IDvalue: <the room UUID from step 4>6. Trigger an inbound
Section titled “6. Trigger an inbound”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.
7. Observe
Section titled “7. Observe”| Where | What you’ll see |
|---|---|
| Inbox | ”Send marketing kit to …” (Marketing) + “First touch: …” (Sales) |
| Rooms → Lead Hand-off | 3 messages: 📥 from Marketing, ✅ from Sales, 📋 enrichment from Sales |
| Audit log | the cross-app invoke + emit + room posts + commitment creates |
| Health → Heartbeats | demo-sales/morning_review row, last_run shows up after 09:00 UTC |
| KPI Cockpit | new “qualified lead” rows pile up |
| Steward | ask “what happened with the lead from [email protected]?” — it’ll narrate |
Local testing
Section titled “Local testing”Both apps ship linkworld.eval.yaml files exercising every branch
without LLMs. Run from each app’s directory:
linkworld eval # 4 scenarios per app, all greenlinkworld eval --filter event # only the on_event scenariosFor the live playground (chat UI in browser):
linkworld chat # mock agent responses from linkworld.dev.jsonlinkworld chat --live # uses real Anthropic via ANTHROPIC_API_KEYWhat this surfaces about your platform
Section titled “What this surfaces about your platform”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_eventhandler 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.