You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/ai-chat/lifecycle-hooks.mdx
+84-9Lines changed: 84 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,12 +4,24 @@ sidebarTitle: "Lifecycle hooks"
4
4
description: "Hook into every stage of a chat agent's run: preload, turn start, turn complete, suspend, resume, and more."
5
5
---
6
6
7
-
`chat.agent({ ... })` accepts a set of lifecycle hooks for persisting state, validating input, transforming messages, and reacting to suspension and resumption. They fire in a fixed order around each turn.
7
+
`chat.agent({ ... })` accepts a set of lifecycle hooks for persisting state, validating input, transforming messages, and reacting to suspension and resumption. They fire at well-defined points in the chat agent's lifetime.
8
+
9
+
**Once per worker process (every fresh run boot):**`onBoot` → `onPreload` (preloaded runs only).
10
+
11
+
**Once per chat (first message of the chat's lifetime):**`onChatStart`.
**Suspend / resume:**`onChatSuspend` fires when the run transitions from idle to suspended (waiting on the next message); `onChatResume` fires on wake.
12
16
17
+
**Three scopes to keep straight:**
18
+
19
+
| Scope | Fires when | Use for |
20
+
| --- | --- | --- |
21
+
|**Process** ([`onBoot`](#onboot)) | Every fresh worker boots — initial, preloaded, and reactive continuation (post-cancel/crash/`endRun`/upgrade). | Initialize `chat.local`, open per-process resources, re-hydrate state from your DB on continuation. |
22
+
|**Chat** ([`onChatStart`](#onchatstart)) | First message of a chat's lifetime. Does NOT fire on continuation runs or OOM retries. | One-time DB rows for the chat, resources tied to the chat's lifetime. |
Every chat lifecycle callback and the `run` payload include `ctx`: the same run context object as `task({ run: (payload, { ctx }) => ... })`. Import the type with `import type { TaskRunContext } from "@trigger.dev/sdk"` (the `Context` export is the same type). Use `ctx` for tags, metadata, or any API that needs the full run record. The string `runId` on chat events is always `ctx.run.id` (both are provided for convenience). See [Task context (`ctx`)](/ai-chat/reference#task-context-ctx) in the API reference.
@@ -18,21 +30,80 @@ Standard [task lifecycle hooks](/tasks/overview) such as `onWait`, `onResume`, `
18
30
19
31
Chat agents also have two dedicated suspension hooks, `onChatSuspend` and `onChatResume`, that fire at the idle-to-suspended transition with full chat context. Use them for resource cleanup (e.g. tearing down sandboxes) and re-initialization. See [onChatSuspend / onChatResume](#onchatsuspend--onchatresume) and the [Code execution sandbox](/ai-chat/patterns/code-sandbox) pattern.
20
32
33
+
## onBoot
34
+
35
+
Fires **once per worker process picking up the chat** — for the initial run, for preloaded runs, AND for reactive continuation runs (post-cancel, crash, `endRun`, `requestUpgrade`, OOM retry). Does NOT fire when the same run resumes from snapshot via the idle-window suspend/resume path — use [`onChatResume`](#onchatsuspend--onchatresume) for that.
36
+
37
+
This is the right place to initialize anything that lives in the JS process for the lifetime of the run: [`chat.local`](/ai-chat/reference#chatlocal) state, DB connections, sandboxes, in-memory caches. It runs before `onPreload`, `onChatStart`, the continuation-wait branch, and any turn — so anything you set up here is available everywhere downstream.
38
+
39
+
<Warning>
40
+
If you initialize `chat.local` only in `onChatStart`, your `run()` will crash on continuation runs with `chat.local can only be modified after initialization`. `onChatStart` is once-per-chat by contract; `chat.local` is per-process and needs `onBoot`.
41
+
</Warning>
42
+
43
+
Branch on `continuation` to decide whether to load existing state from your DB or start fresh:
|`ctx`|`TaskRunContext`| Full task run context. See [reference](/ai-chat/reference#task-context-ctx). |
74
+
|`chatId`|`string`| Chat session ID |
75
+
|`runId`|`string`| The Trigger.dev run ID for this run boot |
76
+
|`chatAccessToken`|`string`| Scoped access token for this run |
77
+
|`clientData`| Typed by `clientDataSchema`| Custom data from the frontend |
78
+
|`continuation`|`boolean`|`true` when this run is taking over from a prior dead run |
79
+
|`previousRunId`|`string \| undefined`| Public id of the prior run when `continuation` is true |
80
+
|`preloaded`|`boolean`| Whether this run was triggered as a preload |
81
+
82
+
<Tip>
83
+
`onBoot` and `onChatStart` are complementary — keep DB-row creation in `onChatStart` (it only needs to happen once per chat) and put process-level setup (`chat.local`, connections, caches) in `onBoot` (it needs to happen on every fresh worker).
84
+
</Tip>
85
+
21
86
## onPreload
22
87
23
-
Fires when a preloaded run starts, before any messages arrive. Use it to eagerly initialize state (DB records, user context) while the user is still typing.
88
+
Fires when a **preloaded run** starts, before any messages arrive. Use it to eagerly create chat-scoped DB rows (the Chat row, the ChatSession row) while the user is still typing — so the very first message lands fast.
24
89
25
90
Preloaded runs are triggered by calling `transport.preload(chatId)` on the frontend. See [Preload](/ai-chat/fast-starts#preload) for details.
26
91
92
+
Per-process state (anything in [`chat.local`](/ai-chat/reference#chatlocal), DB connections, etc.) belongs in [`onBoot`](#onboot) — `onBoot` fires before `onPreload` on every fresh worker, including on continuation runs where `onPreload` never fires.
if (preloaded) return; // Already initialized in onPreload
45
-
// ... non-preloaded initialization
116
+
// ... non-preloaded chat-row initialization
46
117
},
47
118
run: async ({ messages, signal }) => {
48
119
returnstreamText({ model: openai("gpt-4o"), messages, abortSignal: signal });
@@ -63,14 +134,18 @@ Every lifecycle callback receives a `writer`, a lazy stream writer that lets you
63
134
64
135
## onChatStart
65
136
66
-
Fires **exactly once per chat**, on the very first user message of the chat's lifetime, before `run()` executes. Use it for one-time chat setup — create the Chat DB row, initialize per-chat in-memory state, mint resources tied to the chat's lifetime.
137
+
Fires **exactly once per chat**, on the very first user message of the chat's lifetime, before `run()` executes. Use it for one-time chat-scoped setup — create the Chat DB row, mint resources tied to the chat's lifetime.
67
138
68
139
`onChatStart` does **not** fire on:
69
140
70
-
-**Continuation runs** — a new run picking up an existing session after the prior run ended (`chat.endRun`, waitpoint timeout, `chat.requestUpgrade`). The chat already started.
141
+
-**Continuation runs** — a new run picking up an existing session after the prior run ended (`chat.endRun`, waitpoint timeout, `chat.requestUpgrade`, cancel, crash). The chat already started.
71
142
-**OOM-retry attempts** — same chat, same conversation, just on a larger machine.
72
143
73
-
If you need per-turn setup that runs on every turn including continuations, use [`onTurnStart`](#onturnstart) instead.
144
+
For per-process state that has to be initialized on every fresh worker (including continuation runs), use [`onBoot`](#onboot). For per-turn setup, use [`onTurnStart`](#onturnstart).
145
+
146
+
<Warning>
147
+
Do not initialize [`chat.local`](/ai-chat/reference#chatlocal) here. `chat.local` is per-process state that must survive continuation runs, but `onChatStart` only fires on the chat's very first message. Use [`onBoot`](#onboot) instead.
148
+
</Warning>
74
149
75
150
The `preloaded` field tells you whether [`onPreload`](#onpreload) already ran for this chat — useful for skipping setup work that's already done.
Copy file name to clipboardExpand all lines: docs/ai-chat/patterns/database-persistence.mdx
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -30,6 +30,8 @@ Storing the current **`runId`** is optional — useful for telemetry / dashboard
30
30
31
31
## Where each hook writes
32
32
33
+
This pattern covers **durable DB rows** (the conversation and the active session). Per-process in-memory state ([`chat.local`](/ai-chat/reference#chatlocal), DB connection pools, sandboxes, etc.) belongs in [`onBoot`](/ai-chat/lifecycle-hooks#onboot) — it fires on every fresh worker including continuation runs, where `onPreload` and `onChatStart` do not.
34
+
33
35
### `onPreload` (optional)
34
36
35
37
When the user triggers [preload](/ai-chat/fast-starts#preload), the run starts **before** the first user message.
Copy file name to clipboardExpand all lines: docs/ai-chat/reference.mdx
+17-1Lines changed: 17 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,6 +13,7 @@ Options for `chat.agent()`.
13
13
|`id`|`string`| required | Task identifier |
14
14
|`run`|`(payload: ChatTaskRunPayload) => Promise<unknown>`| required | Handler for each turn |
15
15
|`clientDataSchema`|`TaskSchema`| — | Schema for validating and typing `clientData`|
16
+
|`onBoot`|`(event: BootEvent) => Promise<void> \| void`| — | Fires once per worker process — initial, preloaded, AND reactive continuation. Use for `chat.local` init and per-process resources. See [onBoot](/ai-chat/lifecycle-hooks#onboot). |
16
17
|`onPreload`|`(event: PreloadEvent) => Promise<void> \| void`| — | Fires on preloaded runs before the first message |
17
18
|`onChatStart`|`(event: ChatStartEvent) => Promise<void> \| void`| — | Fires once per chat, on the very first user message. Does NOT fire on continuation runs or OOM-retries — see [onChatStart](/ai-chat/lifecycle-hooks#onchatstart). |
18
19
|`onValidateMessages`|`(event: ValidateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>`| — | Validate/transform UIMessages before model conversion. See [onValidateMessages](/ai-chat/lifecycle-hooks#onvalidatemessages)|
@@ -42,7 +43,7 @@ Plus most standard [TaskOptions](/tasks/overview) — `queue`, `machine`, `maxDu
42
43
43
44
## Task context (`ctx`)
44
45
45
-
All **`chat.agent`** lifecycle events (**`onPreload`**, **`onChatStart`**, **`onTurnStart`**, **`onBeforeTurnComplete`**, **`onTurnComplete`**, **`onCompacted`**) and the object passed to **`run`** include **`ctx`**: the same **`TaskRunContext`** shape as the `ctx` in `task({ run: (payload, { ctx }) => ... })`.
46
+
All **`chat.agent`** lifecycle events (**`onBoot`**, **`onPreload`**, **`onChatStart`**, **`onTurnStart`**, **`onBeforeTurnComplete`**, **`onTurnComplete`**, **`onCompacted`**) and the object passed to **`run`** include **`ctx`**: the same **`TaskRunContext`** shape as the `ctx` in `task({ run: (payload, { ctx }) => ... })`.
46
47
47
48
<Note>
48
49
**`onValidateMessages`** does not include `ctx` — it fires before message accumulation and is designed for pure validation/transformation of incoming messages.
@@ -80,6 +81,21 @@ The payload passed to the `run` function.
80
81
|`previousTurnUsage`|`LanguageModelUsage \| undefined`| Token usage from the previous turn (undefined on turn 0) |
81
82
|`totalUsage`|`LanguageModelUsage`| Cumulative token usage across completed turns so far |
`onChatStart` no longer fires on continuation runs (post-`endRun`, post-waitpoint-timeout, post-`chat.requestUpgrade`) or on OOM-retry attempts. It fires **exactly once per chat**, on the very first user message of the chat's lifetime. The `continuation` and `previousRunId` fields on `ChatStartEvent` are now `@deprecated` (always `false` / `undefined` when the hook fires).
447
+
`onChatStart` no longer fires on continuation runs (post-`endRun`, post-waitpoint-timeout, post-`chat.requestUpgrade`, post-cancel, post-crash) or on OOM-retry attempts. It fires **exactly once per chat**, on the very first user message of the chat's lifetime. The `continuation` and `previousRunId` fields on `ChatStartEvent` are now `@deprecated` (always `false` / `undefined` when the hook fires).
448
448
449
-
This makes once-per-chat setup code (create the Chat DB row, init user context) safe to write without continuation gates. Drop any `if (continuation) return;` checks from `onChatStart`:
449
+
This makes once-per-chat setup code (create the Chat DB row, mint chat-scoped resources) safe to write without continuation gates. Drop any `if (continuation) return;` checks from `onChatStart`:
If you need per-turn setup that **does** run on continuations, move it to [`onTurnStart`](/ai-chat/lifecycle-hooks#onturnstart) — that hook still fires on every turn, including the first turn of a continuation run.
465
465
466
+
### Move `chat.local` init from `onChatStart` to `onBoot`
467
+
468
+
Because `onChatStart` no longer fires on continuation runs, **`chat.local`** state initialized there will be missing when a continuation run starts — `run()` then crashes with `"chat.local can only be modified after initialization"`. The fix is to move per-process initialization to the new [`onBoot`](/ai-chat/lifecycle-hooks#onboot) hook, which fires once per worker boot (initial, preloaded, AND continuation):
const user =awaitdb.user.findUnique({ where: { id: clientData.userId } });
484
+
userContext.init({ name: user.name, plan: user.plan }); // ✅ runs on every fresh worker
485
+
}
486
+
```
487
+
488
+
Anything else that's per-process (DB connection pools, sandbox handles, in-memory caches) belongs in `onBoot` for the same reason. Branch on `continuation` inside `onBoot` if you need to re-load state from your DB on takeover.
489
+
466
490
### Client-side `setMessages` doesn't round-trip
467
491
468
492
The new wire makes one thing explicit that was implicit before: **mutating `useChat()`'s messages on the client doesn't change the agent's history.** Full-history mutations were silently overwritten by the wire's accumulator before this release; now they aren't even shipped.
0 commit comments