Files
triggerdotdev--trigger.dev/docs/webhooks/session-routing.mdx

97 lines
4.4 KiB
Plaintext

---
title: "Routing to a session"
description: "Route verified webhook deliveries to a durable per-key session instead of a fresh run."
sidebarTitle: "Session routing"
---
A plain [`webhook()`](/webhooks/overview) runs a fresh, stateless run for every delivery. Sometimes you want the opposite: deliveries that share a key (a customer, an installation, an issue) should land on **one durable [session](/ai-chat/sessions)** and be handled in order, with state carried across them. That is what session routing does.
Instead of a handler, you declare a `chat.event` and list it on an agent. The verified delivery is routed to a find-or-created session and arrives as an [action](/ai-chat/actions).
## Declaring a chat event
`chat.event` describes the routing only: the [source](/webhooks/sources) to verify, a `key` that identifies the session, and a `type` label for the delivered action. It has no handler.
```ts
import { webhooks } from "@trigger.dev/sdk";
import { chat } from "@trigger.dev/sdk/ai";
export const orderEvents = chat.event({
id: "order-events",
source: webhooks.stripe(),
// one session per customer
key: "{body.data.object.customer}",
type: "order.event",
});
```
### The key
The `key` is what makes routing durable: two deliveries that resolve to the same key reach the same session; a different key gets its own. It is a template string, evaluated server-side at ingest, whose `{...}` placeholders read three namespaces:
- `{body.*}`: the parsed body (a bare `{customer}` defaults to the body namespace).
- `{webhook.*}`: endpoint metadata (`externalRef`, `tenantId`, `id`, `source`, `deliveryId`).
- `{header.name}`: an inbound header.
[Filter](/webhooks/filters) expressions read this same parsed body, but spell it `event.*` rather than `{body.*}`.
Compose several placeholders into one key: `"{webhook.externalRef}-{body.issue.id}"`. Each placeholder is checked against the event type at build time, so a bad field is a red squiggle on the `key` line, not a runtime surprise.
### The `type` label
`type` is a name you choose for the delivered action. It flows straight through to `action.type` on the envelope, so your handler can tell webhook actions apart from each other and from browser actions. It is optional and defaults to the descriptor `id`.
## Handling deliveries on an agent
List the descriptor on a [`chat.agent`](/ai-chat/overview) (or a session agent). The delivery arrives at `onAction`, not as a chat message:
```ts
import { chat } from "@trigger.dev/sdk/ai";
import { orderEvents } from "./order-events";
export const orderAgent = chat.agent({
id: "order-agent",
events: [orderEvents],
onAction: async ({ action }) => {
switch (action.type) {
case "order.event":
// action.event is the verified Stripe event, fully typed
console.log("order for", action.event.data.object.customer);
break;
}
},
run: async ({ messages, signal }) => {
/* ... */
},
});
```
`action.type` is a closed union of the `type` labels of the events you listed, and each arm narrows `action.event` to that event's payload type. The full envelope is `{ type, event, source, headers, deliveryId }`.
Because it is an [action](/ai-chat/actions), the handler is not a turn: it can mutate session state and, if you return a `streamText` result, produce a model response. Webhook actions bypass your `actionSchema` (their shape is fixed and already typed).
<Note>
The descriptor never names the agent, and the agent references the descriptor, so there is no
circular dependency. Whichever agent lists it becomes the routing target.
</Note>
## One endpoint per agent
Listing a descriptor on an agent creates that agent's own endpoint, with its own [webhook URL and signing secret](/webhooks/connect). If two agents list the same descriptor, you get two endpoints, each routing to its own agent, and you point the provider at both URLs.
An exported `chat.event` that no agent lists routes nothing. `trigger dev` warns about it (it is not an error, since declaring the descriptor before wiring the agent is a normal step), so wire it onto an agent once you are ready.
## Filtering
A [`filter`](/webhooks/filters) works here exactly as on a fan-out webhook: a non-matching delivery is recorded `FILTERED` and never reaches the session.
```ts
export const orderEvents = chat.event({
id: "order-events",
source: webhooks.stripe(),
key: "{body.data.object.customer}",
type: "order.event",
filter: "event.type == 'payment_intent.succeeded'",
});
```