Skip to content

Cross-app events

If app A wants app B to do something when X happens, the right answer is rarely “let A call B’s API directly”. Use the platform’s event wires: app A emits a named event, app B subscribes_to it, and the wire fires when both tenants have approved the connection.

This is the same machinery that lets demo-marketing qualify a lead and have demo-sales start an outreach cadence — without either app knowing the other’s internals.

NeedPattern
App A wants App B to handle data downstreamemits + subscribes_to wire (this page)
App A wants a synchronous answer from App Bctx.apps.invoke + bilateral grant
App A wants to coordinate humans + multiple agents in a threadctx.rooms.post
Within one app, master agent → specialistctx.team.delegate
React to channel-level inbound (email, WhatsApp, voice)Reacting to emails — NOT this pattern

See cross-app reference for the full API map. This page focuses on the event style (pub/sub, fire-and-forget) — for synchronous RPC use ctx.apps.invoke.

# linkworld.app.yaml — the emitter
emits:
- lead_qualified
- quote_signed

Event names are ^[a-z][a-z0-9_]*$ — lowercase, underscore-separated, no dots. Max 30 events per app.

# In the emitter's handler
await ctx.events.emit("lead_qualified", {
"from": env.sender_name,
"score": 85,
"thread_id": env.channel_thread_id,
"reason": "asked about pricing for 200 seats",
})

Payload must be JSON-serializable. The platform doesn’t transform it — whatever you emit, the subscriber’s handler receives verbatim.

2. Subscriber app declares the subscription

Section titled “2. Subscriber app declares the subscription”
# linkworld.app.yaml — the subscriber
subscribes_to:
- emitter_app: demo-marketing
event_name: lead_qualified
target_agent: bdr # which agent in *this* app handles it
# target_heartbeat: morning_review # OR wake a heartbeat instead

target_agent must be a declared agent in this app. Optional target_heartbeat wakes a specific heartbeat on event arrival instead of invoking the agent — useful when the reaction is part of a batched routine.

from linkworld_sdk import AppEvent
@app.on_event
async def on_event(ctx: Context, ev: AppEvent) -> None:
if ev.event_name != "lead_qualified":
return
if ev.emitter_app_id != "demo-marketing":
return # ignore other emitters (defense in depth)
lead = ev.payload
await ctx.rooms.post(
shared_room_id,
f"✅ Picking up qualified lead {lead['from']} (score {lead['score']})",
from_agent="bdr", # required: must match an agent in your app
)
await ctx.commitments.add(
f"First touch: reach out to {lead['from']}",
due_in_hours=4,
)
app.onEvent(async (ctx, ev) => {
if (ev.event_name !== 'lead_qualified') return
if (ev.emitter_app_id !== 'demo-marketing') return
// … react to ev.payload
})

The manifest entry is just a declaration — “I’m willing to consume this if approved”. The actual wire requires both tenant admins to opt in via workspace-control. Until they do, no events flow.

This is the platform’s privacy guarantee: an emitter can’t push data to an unwilling subscriber, and a subscriber can’t tap into an emitter that didn’t authorize it.

Activation auto-creates + auto-approves the bilateral grant in linkworld_app_event_wires for entries whose target app is also installed and has matching emits. For everything else: admin approval.

The platform tracks X-LW-Chain-Depth headers and rejects events that would extend a chain beyond depth 8. A visited-set on the platform side detects cycles (A emits → B emits → A emits → ...) and breaks them. You don’t write defensive code for this — the platform fails the emit call with a clear error.

Don’t: call another app’s HTTP endpoint directly

Section titled “Don’t: call another app’s HTTP endpoint directly”
# ❌ DON'T do this
await httpx.post(
f"https://app-b.example.com/webhook",
json={"lead": data},
headers={"Authorization": f"Bearer {token}"},
)

Why it’s wrong:

  • You bypass the platform’s audit log — there’s no record of A talking to B.
  • You bypass tenant consent — the tenant admin never approved A→B data flow.
  • You hardcode the recipient’s URL — if app B migrates, A breaks.
  • You hand-roll auth — every breakage there is a security hole.
  • The platform’s chain-depth + cycle-detection can’t see your call.

Always go through ctx.events.emit (for pub/sub) or ctx.apps.invoke (for RPC).

Don’t: invent a transport in your own app records

Section titled “Don’t: invent a transport in your own app records”
# ❌ DON'T do this
# App A writes to a magic "inbox" record_type that App B polls
await ctx.tools.call("write_app_records", record_type="cross_app_inbox", ...)

You’ve rebuilt the wire system as a leaky polling abstraction. No audit, no chain-depth, no consent flow, no event-name validation. The fact that app_records are app-private would have prevented this — if you found a way around it, you’re doing something the platform explicitly designed against.

# ❌ DON'T do this
emits:
- lead_qualified_v2
- lead_qualified_v3

Event names are part of your public API. Subscribers wire up against a specific name; if you rename it, every subscribed app breaks silently. Treat the event name like a database column name — once published, freeze it. Evolve the payload shape backwards-compatibly instead.

Don’t: cram unbounded state into the payload

Section titled “Don’t: cram unbounded state into the payload”
# ❌ DON'T do this
await ctx.events.emit("data_ready", {"rows": entire_query_result})

Wire payloads have a size limit and travel over an HTTP boundary. For large data: emit a reference ({"dataset_id": "..."}) and let the subscriber call ctx.apps.invoke or read shared records to fetch what it needs.

  • Inbound channels (email, WhatsApp, voice, web). Those use on_inbound, not on_event. See Reacting to emails.
  • Same-app collaboration. If the work stays within one app’s agents, use ctx.team.delegate — it’s lighter and doesn’t need the wire/grant approval flow.
  • You need a synchronous answer. ctx.events.emit is fire-and-forget. For “ask app B, wait for its answer, continue”, use ctx.apps.invoke instead — bilateral grant, same auth flow, but request/response semantics.