routingnames the keys an event can be routed to a team by, such as the Zendesk organization.routeKeyson each agent event gives the event’s value for those keys.sourceConnectionsays a Source admin may connect your integration once for the whole Source, and promises your tools stay inside the records routed to each team.
A Source connection is used only by provided agents: agents a Source
publishes from a template that marks your product as Source-owned. A team
member’s widgets, assistant and workflows always use the team’s own
connection.
Declare routing keys
Give each event its route value
routeKeys maps each routing key id to a pure function of the validated payload, like conversation:
{ value, label? }: value is your own id for the thing, matched exactly; label is shown to the admin and never matched. Return null when the event carries no value. A value that is empty, not a string, or longer than 200 characters counts as no value.
A routeKeys key must be one of routing.keys, and an event cannot declare routeKeys unless the integration declares routing. defineIntegration, ekanos validate and the host all refuse it.
What the host does with an event
When your webhook is delivered to the Source’s endpoint (rather than a team’s):- Your handler runs on the Source connection:
ctx.connection.owneris'source'and there is noctx.routing, because no team is known yet. Read what you need from the vendor and raise events as usual. - The host validates each raised event, records it once per
dedupeKeyfor the Source, and reads its value for the Source’s routing key. - Mapped: the event is recorded and dispatched as the mapped team’s, so that team’s triggers, runs, approvals and permissions apply unchanged.
- Unmapped, or no value: the event is unrouted. No team sees it and no agent starts. There is no default team. The Source admin sees it in the unrouted list, maps the value, and can replay it.
routeKeys is ignored there.
The unrouted hook
To tell people an unrouted event is waiting, declarehooks.unrouted on your channel. It posts one fixed internal note where a reply would have gone:
targetis the event’sconversation()target. The host calls the hook only for an event that has one, at most once per event.- The note is fixed text from your code. It is never written by a model.
- Like every channel hook it is best-effort: a hook that throws is logged and ignored.
- It needs
routing: a channel declaringunroutedwithout it is refused.
deliver, so it belongs beside them. There is no run, so unlike the other hooks it receives no run.
Offer a Source connection
true, and sourceConnection requires routing. The host refuses to make your product Source-owned in a template unless you declare it.
scopedTools: true is a promise. A Source connection’s tokens reach every team’s records, so your tools are the boundary. On a Source connection, ctx.routing is { key, values }: the routing key and the values routed to the team the call is made for. Every tool must read and write only records carrying one of ctx.routing.values:
- Searches add the scope to the query (Zendesk:
organization:<id>per value), never filter after fetching. - Reads and writes of one record fetch it first and refuse one outside the scope.
- Empty
valuesmeans the team is mapped to nothing, so nothing is in scope. - On the team’s own connection
ctx.routingis absent and everything the connection reaches is the team’s.
The context on a Source connection
ctx.connection is read-only and set by the host. A host that predates Source connections leaves it unset, which means 'team'. On a Source connection ctx.secrets and ctx.storage are the Source’s: the tokens your onTokens stores there are shared by every team’s provided agents, so never keep per-team state there. In a call with no team (the last two rows), don’t rely on ctx.accountId: the host sets it to an empty string, which names no team, and ctx.storage.user throws there.
Test it
Run a tool, a webhook or a channel hook on a Source connection withcreateMockContext({ connection, routing }), and check route values and the hooks with invokeRouteKeys, invokeUnrouted and invokeHandoff (see Testing). In the harness, triggerMocks.connection and triggerMocks.routing run the Triggers surface the same way, and a delivered webhook shows the value each raised event would be routed by (see Triggers).