@trigger.dev/sdk 4.5.15 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/commonjs/imports/ai-runtime-cjs.cjs.map +1 -1
  2. package/dist/commonjs/imports/ai-runtime.js +2 -0
  3. package/dist/commonjs/v3/ai-shared.d.ts +15 -0
  4. package/dist/commonjs/v3/ai-shared.js +35 -1
  5. package/dist/commonjs/v3/ai-shared.js.map +1 -1
  6. package/dist/commonjs/v3/ai.d.ts +311 -25
  7. package/dist/commonjs/v3/ai.js +1633 -380
  8. package/dist/commonjs/v3/ai.js.map +1 -1
  9. package/dist/commonjs/v3/auth.d.ts +10 -4
  10. package/dist/commonjs/v3/auth.js.map +1 -1
  11. package/dist/commonjs/v3/chat-client.d.ts +11 -2
  12. package/dist/commonjs/v3/chat-client.js +227 -111
  13. package/dist/commonjs/v3/chat-client.js.map +1 -1
  14. package/dist/commonjs/v3/chat-react.d.ts +88 -0
  15. package/dist/commonjs/v3/chat-react.js +130 -1
  16. package/dist/commonjs/v3/chat-react.js.map +1 -1
  17. package/dist/commonjs/v3/chat-server.d.ts +42 -6
  18. package/dist/commonjs/v3/chat-server.js +52 -7
  19. package/dist/commonjs/v3/chat-server.js.map +1 -1
  20. package/dist/commonjs/v3/chat.d.ts +92 -10
  21. package/dist/commonjs/v3/chat.js +328 -73
  22. package/dist/commonjs/v3/chat.js.map +1 -1
  23. package/dist/commonjs/v3/chatSnapshotIo.d.ts +2 -0
  24. package/dist/commonjs/v3/chatSnapshotIo.js +173 -0
  25. package/dist/commonjs/v3/chatSnapshotIo.js.map +1 -0
  26. package/dist/commonjs/v3/chatVersionSkew.d.ts +12 -0
  27. package/dist/commonjs/v3/chatVersionSkew.js +30 -0
  28. package/dist/commonjs/v3/chatVersionSkew.js.map +1 -0
  29. package/dist/commonjs/v3/externalDeploymentId.d.ts +23 -0
  30. package/dist/commonjs/v3/externalDeploymentId.js +43 -0
  31. package/dist/commonjs/v3/externalDeploymentId.js.map +1 -0
  32. package/dist/commonjs/v3/retry.d.ts +4 -0
  33. package/dist/commonjs/v3/retry.js +22 -13
  34. package/dist/commonjs/v3/retry.js.map +1 -1
  35. package/dist/commonjs/v3/sessions.d.ts +15 -2
  36. package/dist/commonjs/v3/sessions.js +12 -1
  37. package/dist/commonjs/v3/sessions.js.map +1 -1
  38. package/dist/commonjs/v3/shared.js +30 -36
  39. package/dist/commonjs/v3/shared.js.map +1 -1
  40. package/dist/commonjs/v3/test/index.d.ts +1 -0
  41. package/dist/commonjs/v3/test/index.js +3 -1
  42. package/dist/commonjs/v3/test/index.js.map +1 -1
  43. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +47 -4
  44. package/dist/commonjs/v3/test/mock-chat-agent.js +116 -10
  45. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  46. package/dist/commonjs/v3/test/test-session-handle.js +6 -0
  47. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  48. package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +40 -0
  49. package/dist/commonjs/v3/test/transcript-storage-tests.js +258 -0
  50. package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -0
  51. package/dist/commonjs/v3/transcriptStorage.d.ts +261 -0
  52. package/dist/commonjs/v3/transcriptStorage.js +374 -0
  53. package/dist/commonjs/v3/transcriptStorage.js.map +1 -0
  54. package/dist/commonjs/version.js +1 -1
  55. package/dist/esm/imports/ai-runtime.d.ts +2 -2
  56. package/dist/esm/imports/ai-runtime.js +2 -2
  57. package/dist/esm/imports/ai-runtime.js.map +1 -1
  58. package/dist/esm/v3/ai-shared.d.ts +15 -0
  59. package/dist/esm/v3/ai-shared.js +33 -0
  60. package/dist/esm/v3/ai-shared.js.map +1 -1
  61. package/dist/esm/v3/ai.d.ts +311 -25
  62. package/dist/esm/v3/ai.js +1625 -377
  63. package/dist/esm/v3/ai.js.map +1 -1
  64. package/dist/esm/v3/auth.d.ts +10 -4
  65. package/dist/esm/v3/auth.js.map +1 -1
  66. package/dist/esm/v3/chat-client.d.ts +11 -2
  67. package/dist/esm/v3/chat-client.js +228 -112
  68. package/dist/esm/v3/chat-client.js.map +1 -1
  69. package/dist/esm/v3/chat-react.d.ts +88 -0
  70. package/dist/esm/v3/chat-react.js +127 -1
  71. package/dist/esm/v3/chat-react.js.map +1 -1
  72. package/dist/esm/v3/chat-server.d.ts +42 -6
  73. package/dist/esm/v3/chat-server.js +53 -8
  74. package/dist/esm/v3/chat-server.js.map +1 -1
  75. package/dist/esm/v3/chat.d.ts +92 -10
  76. package/dist/esm/v3/chat.js +329 -74
  77. package/dist/esm/v3/chat.js.map +1 -1
  78. package/dist/esm/v3/chatSnapshotIo.d.ts +2 -0
  79. package/dist/esm/v3/chatSnapshotIo.js +165 -0
  80. package/dist/esm/v3/chatSnapshotIo.js.map +1 -0
  81. package/dist/esm/v3/chatVersionSkew.d.ts +12 -0
  82. package/dist/esm/v3/chatVersionSkew.js +27 -0
  83. package/dist/esm/v3/chatVersionSkew.js.map +1 -0
  84. package/dist/esm/v3/externalDeploymentId.d.ts +23 -0
  85. package/dist/esm/v3/externalDeploymentId.js +38 -0
  86. package/dist/esm/v3/externalDeploymentId.js.map +1 -0
  87. package/dist/esm/v3/retry.d.ts +4 -0
  88. package/dist/esm/v3/retry.js +14 -8
  89. package/dist/esm/v3/retry.js.map +1 -1
  90. package/dist/esm/v3/sessions.d.ts +15 -2
  91. package/dist/esm/v3/sessions.js +11 -1
  92. package/dist/esm/v3/sessions.js.map +1 -1
  93. package/dist/esm/v3/shared.js +17 -23
  94. package/dist/esm/v3/shared.js.map +1 -1
  95. package/dist/esm/v3/test/index.d.ts +1 -0
  96. package/dist/esm/v3/test/index.js +1 -0
  97. package/dist/esm/v3/test/index.js.map +1 -1
  98. package/dist/esm/v3/test/mock-chat-agent.d.ts +47 -4
  99. package/dist/esm/v3/test/mock-chat-agent.js +118 -12
  100. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  101. package/dist/esm/v3/test/test-session-handle.js +6 -0
  102. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  103. package/dist/esm/v3/test/transcript-storage-tests.d.ts +40 -0
  104. package/dist/esm/v3/test/transcript-storage-tests.js +255 -0
  105. package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -0
  106. package/dist/esm/v3/transcriptStorage.d.ts +261 -0
  107. package/dist/esm/v3/transcriptStorage.js +360 -0
  108. package/dist/esm/v3/transcriptStorage.js.map +1 -0
  109. package/dist/esm/version.js +1 -1
  110. package/docs/ai-chat/actions.mdx +55 -23
  111. package/docs/ai-chat/anatomy.mdx +47 -27
  112. package/docs/ai-chat/backend.mdx +125 -48
  113. package/docs/ai-chat/background-injection.mdx +122 -19
  114. package/docs/ai-chat/client-protocol.mdx +5 -4
  115. package/docs/ai-chat/compaction.mdx +13 -7
  116. package/docs/ai-chat/custom-agents.mdx +23 -0
  117. package/docs/ai-chat/fast-starts.mdx +27 -20
  118. package/docs/ai-chat/frontend.mdx +19 -14
  119. package/docs/ai-chat/how-it-works.mdx +1 -1
  120. package/docs/ai-chat/lifecycle-hooks.mdx +6 -2
  121. package/docs/ai-chat/migrating-from-a-route-handler.mdx +16 -14
  122. package/docs/ai-chat/migrating-from-hydrate-messages.mdx +299 -0
  123. package/docs/ai-chat/patterns/database-persistence.mdx +24 -9
  124. package/docs/ai-chat/patterns/native-compaction.mdx +310 -0
  125. package/docs/ai-chat/patterns/persistence-and-replay.mdx +34 -23
  126. package/docs/ai-chat/patterns/skills.mdx +7 -10
  127. package/docs/ai-chat/patterns/version-upgrades.mdx +79 -6
  128. package/docs/ai-chat/pending-messages.mdx +3 -3
  129. package/docs/ai-chat/prompt-caching.mdx +23 -25
  130. package/docs/ai-chat/quick-start.mdx +11 -11
  131. package/docs/ai-chat/reference.mdx +69 -10
  132. package/docs/ai-chat/sessions.mdx +6 -1
  133. package/docs/ai-chat/side-channels.mdx +1 -1
  134. package/docs/ai-chat/testing.mdx +2 -1
  135. package/docs/ai-chat/tools.mdx +18 -13
  136. package/docs/ai-chat/transcript-storage.mdx +247 -0
  137. package/docs/ai-chat/upgrade-guide.mdx +2 -2
  138. package/docs/apikeys.mdx +27 -45
  139. package/docs/config/extensions/custom.mdx +14 -1
  140. package/docs/config/extensions/syncEnvVars.mdx +6 -0
  141. package/docs/deploy-environment-variables.mdx +23 -2
  142. package/docs/deployment/overview.mdx +4 -8
  143. package/docs/deployment/preview-branches.mdx +4 -4
  144. package/docs/deployment/version-skew-protection.mdx +62 -0
  145. package/docs/github-integration.mdx +1 -1
  146. package/docs/introduction.mdx +58 -152
  147. package/docs/manual-setup.mdx +7 -7
  148. package/docs/mcp-tools.mdx +9 -0
  149. package/docs/quick-start.mdx +3 -3
  150. package/docs/realtime/auth.mdx +1 -1
  151. package/docs/reports.mdx +157 -0
  152. package/docs/self-hosting/security.mdx +17 -0
  153. package/docs/tasks/scheduled.mdx +24 -0
  154. package/docs/triggering.mdx +1 -1
  155. package/docs/troubleshooting-zod.mdx +158 -0
  156. package/docs/troubleshooting.mdx +1 -1
  157. package/package.json +4 -4
  158. package/skills/trigger-authoring-chat-agent/SKILL.md +41 -29
  159. package/skills/trigger-chat-agent-advanced/SKILL.md +54 -19
@@ -0,0 +1,247 @@
1
+ ---
2
+ title: "Transcript storage"
3
+ sidebarTitle: "Transcript storage"
4
+ description: "Where a chat.agent conversation is kept: the UIMessages the runtime saves, the platform default, reading history back, and bringing your own database through the TranscriptStorage adapter."
5
+ ---
6
+
7
+ ## Why a conversation needs a home
8
+
9
+ A `chat.agent` conversation outlives a single run. One run answers many turns and survives the idle gaps between them, but a run does end eventually (a version upgrade, its turn limit, a crash), and the next message then boots a fresh run with nothing in memory (see [How it works](/ai-chat/how-it-works)). For that new run to answer in context, the conversation so far has to be read back from somewhere durable. The same store is what a page reload and the dashboard read to show history.
10
+
11
+ That somewhere is a **transcript storage**. You get one by default with no setup: the platform keeps the conversation as a snapshot in object storage, the same blob the Sessions view in the dashboard renders. Bring your own when you want the conversation in your own database instead.
12
+
13
+ ## What gets saved
14
+
15
+ The transcript is a list of **`UIMessage`s**, keyed by `chatId`. A `UIMessage` is the rich, renderable message the frontend works with: an `id`, a `role`, and an array of `parts` (text, reasoning, tool calls and their results, and any custom `data-*` parts). It is the same shape your React app holds and the same shape the dashboard renders, so what you store is exactly what a user sees.
16
+
17
+ <Note>
18
+ `UIMessage`s are not what the model reads. Each turn the runtime derives a `ModelMessage[]` from the transcript, the flattened `{ role, content }` form an LLM takes, and hands it to your `run()` as `messages`. The transcript storage never deals in `ModelMessage`s. It holds the UI messages; the model's view is derived from them.
19
+ </Note>
20
+
21
+ Keeping the UI shape is deliberate. It is lossless (a tool call and its result survive as parts), it is what renders, and the model's view can be rebuilt from it. Two things cannot be rebuilt from the messages alone, so the runtime hands them to the storage as well:
22
+
23
+ - **`state`**: an opaque record for what the model saw that the transcript does not capture, a [compaction](/ai-chat/compaction) summary and [injected context](/ai-chat/background-injection). Store it as-is and give it back on load.
24
+ - **cursors**: the stream positions the next run resumes from. Persist them opaquely; a storage never reads them.
25
+
26
+ So a save is: the messages, a `state` blob, and two cursors. Nothing else.
27
+
28
+ ## The default storage
29
+
30
+ Do nothing and you get the platform snapshot: the whole conversation written to object storage after each change, read back when a run continues. It is the blob the dashboard's Sessions view renders, and it needs no configuration.
31
+
32
+ ```ts
33
+ import { chat } from "@trigger.dev/sdk/ai";
34
+ import { anthropic } from "@ai-sdk/anthropic";
35
+
36
+ export const myChat = chat.agent({
37
+ id: "my-chat",
38
+ run: async ({ messages, signal, streamText }) =>
39
+ streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal }),
40
+ });
41
+ ```
42
+
43
+ The default rewrites the whole conversation on every turn. That is fine for most chats and costs one write. When it stops being fine, or when you want the conversation in a database you already run, you bring your own.
44
+
45
+ ## Bring your own storage
46
+
47
+ Set `storage` on the agent to persist the conversation yourself:
48
+
49
+ ```ts
50
+ import { chat } from "@trigger.dev/sdk/ai";
51
+ import { anthropic } from "@ai-sdk/anthropic";
52
+ import { myTranscriptStorage } from "./transcript-storage";
53
+
54
+ export const myChat = chat.agent({
55
+ id: "my-chat",
56
+ storage: myTranscriptStorage,
57
+ run: async ({ messages, signal, streamText }) =>
58
+ streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal }),
59
+ });
60
+ ```
61
+
62
+ Reasons to:
63
+
64
+ - **Your database is the source of truth.** History lives next to the rest of your data, queryable, backed up, and deletable on your terms.
65
+ - **Cheaper writes on long chats.** A row-per-message store writes only what changed on a turn instead of rewriting the whole conversation.
66
+ - **Render history in one query** from your own tables, the same `load` the runtime uses.
67
+ - **Own the model's context** for branching, trust boundaries, or rollback (see [Owning the model's context](#owning-the-models-context)).
68
+
69
+ The runtime drives the storage. You never decide when to write, what a regenerate means for your rows, or how a crash mid-answer is recovered. Those decisions are the same for every backend, so they live in the runtime; your job is to store what it hands you and give it back.
70
+
71
+ ## The interface
72
+
73
+ ```ts
74
+ type TranscriptStorage<TClientData = unknown> = {
75
+ load(
76
+ scope: { chatId: string; clientData: TClientData },
77
+ opts?: { limit?: number; before?: string }
78
+ ): Promise<{
79
+ messages: UIMessage[];
80
+ state: unknown | null;
81
+ cursors?: { lastOutEventId?: string; lastInEventId?: string };
82
+ nextCursor?: string;
83
+ }>;
84
+
85
+ save(
86
+ ctx: {
87
+ chatId: string;
88
+ clientData: TClientData;
89
+ turn: number;
90
+ trigger: "submit-message" | "regenerate-message" | "action";
91
+ runId: string;
92
+ ctx: TaskRunContext;
93
+ },
94
+ changeset: {
95
+ reason: "turn-start" | "turn-complete" | "turn-error" | "action" | "compaction" | "recovery";
96
+ changes: TranscriptChange[];
97
+ transcript: { entries: Array<{ id: string; final: boolean; message: UIMessage }>; state: unknown | null };
98
+ cursors?: { lastOutEventId?: string; lastInEventId?: string };
99
+ }
100
+ ): Promise<void>;
101
+
102
+ loadContext?(
103
+ scope: { chatId: string; clientData: TClientData },
104
+ event: LoadContextEvent
105
+ ): Promise<UIMessage[]>;
106
+ };
107
+
108
+ type TranscriptChange =
109
+ | { op: "put"; message: UIMessage; final?: boolean }
110
+ | { op: "remove"; id: string }
111
+ | { op: "truncateAfter"; afterId: string }
112
+ | { op: "state"; value: unknown | null };
113
+ ```
114
+
115
+ `load` returns the conversation. `save` records a change to it. `loadContext` is optional and covered [below](#owning-the-models-context). All the types are exported from `@trigger.dev/sdk/ai`.
116
+
117
+ `scope` is the tenant of a read: the `chatId` and the `clientData` your app passed. `ctx` on a save is the same plus the run it happened in. `clientData` is how the runtime hands you the tenant; use it to scope or authorize where your backend needs to.
118
+
119
+ ## What the runtime hands `save`
120
+
121
+ A changeset carries the same save two ways, and a storage uses whichever suits its shape.
122
+
123
+ `changes` is the ordered list of what changed since the last save. A row-per-message store applies them, as one transaction where the backend supports one:
124
+
125
+ | Change | Meaning |
126
+ | --- | --- |
127
+ | `put` | Upsert by `message.id`. An unknown id appends at the end; a known id is replaced in place. `final` is `false` for a partial answer captured from a turn that failed or was stopped, and `true` otherwise. |
128
+ | `remove` | Delete by id. A no-op for an unknown id. |
129
+ | `truncateAfter` | Drop every message ordered after `afterId`. This is what an undo or a regenerate becomes. A no-op for an unknown id. |
130
+ | `state` | Replace the runtime's opaque record; `null` clears it. |
131
+
132
+ `transcript` is the whole conversation as it stands after those changes, `entries` plus `state`. A store that keeps the conversation as one document (object storage, a key-value store, a JSON column) writes it as-is and keeps no state of its own between saves. The default storage is exactly that: it serialises `transcript` and rewrites the blob.
133
+
134
+ The changes are the intent, spelled out. A normal turn is two `put`s, the user's message and the assistant's answer, split across the turn's two saves. A steering message the user sent mid-turn is another `put` in the same changeset. An undo through `chat.history.slice(0, -2)` is one `truncateAfter`. A regenerate is a `truncateAfter` and a `put`. A tool approval that updates the assistant message in place is one `put` for that id. Messages are addressed by id; how you order rows is your concern.
135
+
136
+ Every turn saves twice. The `turn-start` save carries the message being answered, before the model runs. The `turn-complete` save carries the answer. Both `put` the same user message id, and a `put` upserts, so a storage that applies changes in order needs no special handling for the repeat.
137
+
138
+ The `turn-start` save is what makes a reload during an answer show the question that is being answered. It leaves `cursors` on the previous turn's position, because the answer's own cursor does not exist yet, so a reload mid-answer still resumes from the last completed turn rather than skipping chunks it never received.
139
+
140
+ A few properties worth knowing:
141
+
142
+ - The `turn-start` save runs alongside the model rather than before it, so it costs no time to first token. Nothing from the turn reaches the browser until it settles, which is what makes the question durable before the answer can render. A save that fails or runs long lets the answer through rather than stalling the conversation.
143
+ - The `turn-complete` save happens after the turn's answer has reached the browser, so it never delays the response. The runtime awaits each `save` before the run suspends.
144
+ - A `save` that throws is logged and the turn continues. The changes fold into the next changeset, and every change is idempotent, so a retried changeset converges on the same result.
145
+ - A `load` that throws boots the run from the durable stream's recent tail rather than failing.
146
+
147
+ ## Reading the transcript
148
+
149
+ `load` is the one read for every backend, the default included. Call it on your server, scoped to the signed-in user through `clientData`, and pass the result to the browser:
150
+
151
+ ```ts app/actions.ts
152
+ "use server";
153
+ import { chat, defaultStorage } from "@trigger.dev/sdk/ai";
154
+
155
+ export const loadTranscript = chat.createLoadTranscriptAction(defaultStorage, { limit: 50 });
156
+ ```
157
+
158
+ ```tsx app/chat/[chatId]/ChatPage.tsx
159
+ "use client";
160
+ import { useLoadTranscript, useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
161
+ import { loadTranscript } from "@/app/actions";
162
+
163
+ export function ChatPage({ chatId }: { chatId: string }) {
164
+ const transport = useTriggerChatTransport({ task: "my-chat", accessToken, startSession });
165
+ const { messages, isLoading, nextCursor } = useLoadTranscript(chatId, loadTranscript, {
166
+ transport,
167
+ });
168
+ if (isLoading) return <Spinner />;
169
+ return <ChatView chatId={chatId} initialMessages={messages} transport={transport} />;
170
+ }
171
+ ```
172
+
173
+ <Warning>
174
+ The action receives `chatId` from the browser, so authorize it before returning: check that the signed-in user owns this chat. `defaultStorage` loads purely by `chatId` and does no tenant check of its own, so an exported action with no authorization lets any authenticated user read any chat's transcript. A custom storage can enforce tenancy inside `load` using `clientData`, but the server action is still the place to reject a `chatId` the caller may not read.
175
+ </Warning>
176
+
177
+ `limit` returns the most recent messages and a `nextCursor`; pass it as `before` for the page before that one. With the default storage, a paged read is served by the platform, so a long conversation is not downloaded in full to render its last fifty messages. A paged read returns the transcript only: `state` is always `null`, because the model lane is not part of what a page renders, and the platform reads just the bytes holding that page rather than the whole conversation. The runtime reads the state separately when it restores context at boot. When you pass `transport` and it already knows the session, the hook seeds its resume cursor from the transcript, so the live subscription opens just past the persisted history instead of replaying it.
178
+
179
+ Swap `defaultStorage` for your own storage and nothing else about the read changes.
180
+
181
+ <Warning>
182
+ The saved format changed in this release, and an older SDK cannot read it. Rolling a deployment back to a version from before this release means its runs will not find a readable transcript for conversations already saved by the newer one, and will continue from the live stream tail instead, so earlier history is lost for those conversations. Roll forward rather than back, or keep your own transcript storage.
183
+ </Warning>
184
+
185
+ The default storage is deliberately basic about long conversations. Once compaction has run, it keeps roughly the last hundred messages and drops the rest, so what it rewrites on each save stops growing. A conversation that never compacts is kept whole. If your app renders history further back than that, give the agent your own storage and keep the messages yourself.
186
+
187
+ ## Owning the model's context
188
+
189
+ By default the model's context each turn is the transcript the runtime accumulated, converted to `ModelMessage`s. A storage that declares `loadContext` takes that over: the runtime calls it on every turn and action, with the messages the frontend sent and the transcript the runtime had, and uses the `UIMessage`s it returns as the conversation (converting them to `ModelMessage`s the same way). Reach for it when your database decides what the model sees, for branching conversations, a trust boundary where the browser's history is not to be believed, or a curated context window.
190
+
191
+ ```ts
192
+ const storage: TranscriptStorage<{ userId: string }> = {
193
+ load: (scope, opts) => rows.load(scope, opts),
194
+ save: (ctx, changeset) => rows.save(ctx, changeset),
195
+ loadContext: async ({ chatId, clientData }, { incomingMessages }) => {
196
+ const branch = await rows.activeBranch(chatId, clientData.userId);
197
+ return [...branch, ...incomingMessages];
198
+ },
199
+ };
200
+ ```
201
+
202
+ `save` keeps receiving every change, and crash recovery keeps running. This is the replacement for the deprecated [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) hook; setting both `hydrateMessages` and `storage` on an agent is a startup error.
203
+
204
+ ## Writing your own storage
205
+
206
+ The contract is small and the conformance suite checks it. Point the suite at a factory for your storage and run it under vitest or jest:
207
+
208
+ ```ts transcript-storage.test.ts
209
+ import { runTranscriptStorageTests } from "@trigger.dev/sdk/ai/test";
210
+ import { postgresTranscriptStorage } from "./transcript-storage";
211
+
212
+ runTranscriptStorageTests(() => postgresTranscriptStorage(process.env.TEST_DATABASE_URL!));
213
+ ```
214
+
215
+ The suite covers appends and in-place replacement, idempotent `remove` and `truncateAfter`, `state` round-trips, cursors, replaying the same changeset twice, paging, and chat isolation. `memoryTranscriptStorage()` is the reference implementation, and it is handy in your own tests to see exactly what the runtime hands a storage.
216
+
217
+ A few things to get right:
218
+
219
+ - Pick one view and stay with it. Apply `changes` if you store rows, write `transcript` if you store a document; don't mix them within one save.
220
+ - `put` for a known id replaces the message in place; position and ordering don't change.
221
+ - Order is the message's position in the transcript. A document store gets it from `transcript.entries`. A row store needs an order column set once, when a `put` first inserts an id, following the order the `put`s arrive in, and left unchanged when a later `put` replaces that id in place. Don't sort by a write timestamp: a replaced message has to keep its place, and a steering message sent mid-turn sorts before the answer it shaped even though its row is written later.
222
+ - `truncateAfter` and `remove` are idempotent. Applying a changeset twice gives the same result as applying it once.
223
+ - `load` with no options returns the whole conversation in order. With `limit`, return the most recent messages and a `nextCursor` (the id of the oldest returned message) when earlier messages exist.
224
+ - Scope reads and writes by `clientData` where your backend enforces tenancy.
225
+
226
+ ## Guarantees and limits
227
+
228
+ - Crash recovery of a half-written answer is runtime-owned in every configuration. It comes from the durable session stream, which no application database can reconstruct. A storage holds settled turns; the runtime overlays the recovered tail and hands it to `save` like any other change.
229
+ - Bringing your own database does not remove platform custody. Session streams still hold message content for their retention window.
230
+ - The default storage rewrites the whole conversation each turn. A row-per-message storage writes only what changed. That is the reason to plug in your own.
231
+
232
+ ## Migrating from hydrateMessages
233
+
234
+ `hydrateMessages` keeps working with a one-time deprecation warning. Crash recovery runs for it, but the runtime does not write to your store on its behalf. The move, in short:
235
+
236
+ 1. Implement `TranscriptStorage` over your existing tables. The writes you did in hooks become `save`; the read your hook did becomes `load`. Add `loadContext` only if your database decides what the model sees each turn.
237
+ 2. Set `storage` on the agent and remove `hydrateMessages`. Setting both is an error.
238
+ 3. Delete the recovery, compaction and cursor code the runtime now owns, and run `runTranscriptStorageTests` against your implementation.
239
+
240
+ The [migration guide](/ai-chat/migrating-from-hydrate-messages) walks through each step with code, and lists what to delete from each hook.
241
+
242
+ ## See also
243
+
244
+ - [Persistence and replay](/ai-chat/patterns/persistence-and-replay): how the runtime rebuilds a conversation when a new run boots
245
+ - [Database persistence](/ai-chat/patterns/database-persistence): the hook-based pattern and how it relates
246
+ - [Actions](/ai-chat/actions#actions-and-persistence): what an undo or regenerate becomes in the changeset
247
+ - [Compaction](/ai-chat/compaction): the summary the runtime keeps in `state`
@@ -298,8 +298,8 @@ and direct API consumers.
298
298
  fire at the same lifecycle points.
299
299
  - `onAction` is still defined the same way, but its semantics changed
300
300
  in the [May 6 prerelease](/ai-chat/changelog) — actions are no longer
301
- turns, and `onAction` returning a `StreamTextResult` produces a model
302
- response.
301
+ turns. To answer after an action's edit, return `chat.turn()`; returning
302
+ a `StreamTextResult` is no longer supported.
303
303
  - `chat.customAgent({...})` and the `chat.createSession(payload, ...)`
304
304
  helper for building a session loop manually inside a custom agent.
305
305
  - `chat.defer` (deferred work) and `chat.history` (imperative history
package/docs/apikeys.mdx CHANGED
@@ -10,17 +10,35 @@ description: "Authenticate backend requests with environment-specific API keys."
10
10
  environment variable, never commit them to source control, and never expose them in frontend code.
11
11
  </Warning>
12
12
 
13
- ## Find your API keys
13
+ ## Create an API key
14
14
 
15
- Open your project in the dashboard, select an environment, and open the [**API keys**](https://cloud.trigger.dev/_/apikeys) page.
15
+ Create a separate named key for each service or integration that accesses Trigger.dev.
16
16
 
17
- API keys belong to one environment. A Development key cannot access Production, and a Production key cannot access Staging.
17
+ API keys belong to one environment. A Development key cannot access Production, and a Production key cannot access Staging. Every team member has their own Development environment, so create local-development keys in your own environment.
18
18
 
19
19
  <Note>
20
- Every team member has their own Development environment and keys. Copy the Development key from
21
- your own API keys page so local requests run against your machine.
20
+ Creating and revoking keys requires permission to manage API keys for the selected environment.
21
+ The dashboard disables these actions when your role does not have permission.
22
22
  </Note>
23
23
 
24
+ <Steps titleSize="h3">
25
+ <Step title="Open the API keys page">
26
+ Select the project and environment the integration needs to access, then open [**API keys**](https://cloud.trigger.dev/_/apikeys).
27
+ </Step>
28
+ <Step title="Create the key">
29
+ Click **New API key**, enter a descriptive name, and optionally set an expiration date. Names can
30
+ contain up to 64 characters.
31
+ </Step>
32
+ <Step title="Choose its access">
33
+ Select the narrowest access preset that supports the integration. For task-aware presets, choose
34
+ all tasks or up to 10 task identifiers.
35
+ </Step>
36
+ <Step title="Copy and store the secret">
37
+ Copy the key into your secret manager or backend environment. Trigger.dev shows the complete
38
+ value only once.
39
+ </Step>
40
+ </Steps>
41
+
24
42
  ## Configure the SDK
25
43
 
26
44
  Set `TRIGGER_SECRET_KEY` in your backend environment. The SDK reads it automatically for operations such as triggering tasks and retrieving runs.
@@ -49,38 +67,12 @@ await tasks.trigger<typeof sendEmail>("send-email", {
49
67
  If you self-host Trigger.dev, set `TRIGGER_API_URL` or pass `baseURL` to `configure`:
50
68
 
51
69
  ```bash .env
52
- TRIGGER_SECRET_KEY="tr_prod_…"
70
+ TRIGGER_SECRET_KEY="tr_prod_sk_…"
53
71
  TRIGGER_API_URL="https://trigger.example.com"
54
72
  ```
55
73
 
56
74
  The default API URL is `https://api.trigger.dev`.
57
75
 
58
- ## Create a key
59
-
60
- Create a separate key for each service or integration that accesses Trigger.dev.
61
-
62
- <Note>
63
- Creating and revoking keys requires permission to manage API keys for the selected environment.
64
- The dashboard disables these actions when your role does not have permission.
65
- </Note>
66
-
67
- <Steps titleSize="h3">
68
- <Step title="Open the API keys page">
69
- Select the project and environment the integration needs to access, then open [**API keys**](https://cloud.trigger.dev/_/apikeys).
70
- </Step>
71
- <Step title="Create the key">
72
- Click **New API key**, enter a descriptive name, and optionally set an expiration date. Names can
73
- contain up to 64 characters.
74
- </Step>
75
- <Step title="Choose its access">
76
- Select an access preset. For task-aware presets, choose all tasks or up to 10 task identifiers.
77
- </Step>
78
- <Step title="Copy and store the secret">
79
- Copy the key into your secret manager or backend environment. Trigger.dev shows the complete
80
- value only once.
81
- </Step>
82
- </Steps>
83
-
84
76
  ## Access presets
85
77
 
86
78
  Access presets define what a key can do. Some presets require a paid plan. The dashboard shows which presets your organization can use — see [pricing](https://trigger.dev/pricing).
@@ -124,28 +116,18 @@ Revoking a key takes effect immediately and cannot be reversed. Requests using t
124
116
 
125
117
  Removing a team member does not revoke keys they created. Review and revoke their keys separately when their access changes.
126
118
 
127
- ## Root keys
128
-
129
- <Warning>
130
- Root keys are legacy, and are likely to be deprecated in the future. We recommend against using them.
131
- </Warning>
132
-
133
- Each environment has a single legacy root key. It can be regenerated, which creates a new value immediately. The previous root key remains valid for 24 hours so you can update services without downtime, then stops authenticating.
134
-
135
- Public access tokens signed with the previous root key remain valid until the earlier of their own expiration and the end of the 24-hour grace period.
136
-
137
119
  ## Create public access tokens
138
120
 
139
121
  API keys can be used to create scoped [Public Access Tokens](/realtime/auth) using `auth.createPublicToken()`.
140
122
 
141
- To do so with the newer non-root keys, you must use `@trigger.dev/sdk` version 4.5.8 or later. Creating public tokens with non-root keys has the following restrictions:
123
+ Use `@trigger.dev/sdk` version 4.5.8 or later to create public tokens with environment API keys. Public tokens have the following restrictions:
142
124
 
143
125
  - The token must request at least one scope.
144
126
  - Its scopes cannot exceed the key's access.
145
127
  - Its expiration cannot exceed 30 days.
146
128
 
147
129
  <Note>
148
- Revoking or expiring an API key does not revoke tokens it already created. Those tokens remain valid until their own expiration, unless the environment's root key is regenerated.
130
+ Revoking or expiring an API key does not revoke tokens it already created. Those tokens remain valid until their own expiration.
149
131
  </Note>
150
132
 
151
133
  ## Target Preview and Development branches
@@ -163,7 +145,7 @@ The SDK sends the branch automatically. When calling the API directly, send the
163
145
 
164
146
  Self-hosted installations support multiple keys with **No restrictions**. The restricted access presets are available in Trigger.dev Cloud.
165
147
 
166
- Keep your instance and SDK current before creating keys. Calling a public-token API with a key on a server that does not support server-minted tokens returns an upgrade error; use the root key until the server is upgraded.
148
+ Keep your instance and SDK current before creating keys. Calling a public-token API with a key on a server that does not support server-minted tokens returns an upgrade error. Upgrade the instance before creating public tokens with an environment API key.
167
149
 
168
150
  ## Security recommendations
169
151
 
@@ -80,7 +80,7 @@ export default defineConfig({
80
80
  extensions: [
81
81
  {
82
82
  name: "my-extension",
83
- externalsForTarget: async (target) => {
83
+ externalsForTarget: (target) => {
84
84
  return ["my-dependency"];
85
85
  },
86
86
  },
@@ -89,6 +89,19 @@ export default defineConfig({
89
89
  });
90
90
  ```
91
91
 
92
+ ### installedPackagesForTarget
93
+
94
+ This tells build diagnostics which packages your extension installs into the deployed image for a given target, so warnings (like the one for packages loaded via `createRequire()`) don't fire for packages that will actually be available at runtime. The bundler ignores this hook, so declaring it never changes the build output. Only implement it if your extension installs packages; extensions without it are assumed to install none.
95
+
96
+ ```ts
97
+ {
98
+ name: "my-extension",
99
+ installedPackagesForTarget: (target) => {
100
+ return target === "deploy" ? ["my-dependency"] : [];
101
+ },
102
+ }
103
+ ```
104
+
92
105
  ### onBuildStart
93
106
 
94
107
  This hook runs before the build starts. It receives the `BuildContext` object as an argument.
@@ -63,6 +63,12 @@ export default defineConfig({
63
63
 
64
64
  In this example we're using env vars from [Infisical](https://infisical.com).
65
65
 
66
+ <Tip>
67
+ Infisical also offers a native [Secret Sync](/deploy-environment-variables#infisical-secret-sync)
68
+ that pushes secrets to Trigger.dev without a build extension or a redeploy. Use `syncEnvVars` when
69
+ you want to resolve secrets at deploy time in code, or for a service without a native sync.
70
+ </Tip>
71
+
66
72
  ```ts trigger.config.ts
67
73
  import { defineConfig } from "@trigger.dev/sdk";
68
74
  import { syncEnvVars } from "@trigger.dev/build/extensions/core";
@@ -184,14 +184,35 @@ For more information about the context object, see the [Context documentation](/
184
184
 
185
185
  ### Sync env vars from another service
186
186
 
187
- You could use the SDK functions above but it's much easier to use our `syncEnvVars` build extension in your `trigger.config` file.
187
+ There are two ways to pull secrets from another service into Trigger.dev: a native **Secret Sync** (currently [Infisical](https://infisical.com)), or the `syncEnvVars` build extension for any other service.
188
+
189
+ #### Infisical Secret Sync
190
+
191
+ If your secrets live in [Infisical](https://infisical.com), sync them natively, without a build extension or a redeploy. You configure the sync in the Infisical dashboard, and it pushes secrets straight to your [Environment Variables page](#in-the-dashboard) in Trigger.dev. When a secret changes in Infisical the sync updates the matching variable, and your tasks pick up the new value on their next run.
192
+
193
+ Set it up in Infisical in two steps:
194
+
195
+ 1. Add a **Trigger.dev App Connection** using a Trigger.dev Personal Access Token from your account settings. Self-hosted instances are supported.
196
+ 2. Create a **Secret Sync**: choose the connection, pick the target organization, project and environment (Production, Staging, Development, or Preview), and set the secret path to sync.
197
+
198
+ Follow the [Trigger.dev Secret Sync guide](https://infisical.com/docs/integrations/secret-syncs/trigger-dev) in the Infisical docs for the full walkthrough.
199
+
200
+ <Note>
201
+ A Secret Sync only overwrites the keys it manages, leaving variables you set manually untouched.
202
+ Synced variables are marked as [secret](#secret-environment-variables) by default, so they appear
203
+ redacted on the Environment Variables page.
204
+ </Note>
205
+
206
+ #### Using the `syncEnvVars` build extension
207
+
208
+ For any other service, use our `syncEnvVars` build extension in your `trigger.config` file to resolve secrets at deploy time.
188
209
 
189
210
  <Note>
190
211
  To use the `syncEnvVars` build extension, you should first install the `@trigger.dev/build`
191
212
  package into your devDependencies.
192
213
  </Note>
193
214
 
194
- In this example we're using env vars from [Infisical](https://infisical.com).
215
+ In this example we're using env vars from [Infisical](https://infisical.com), but you can adapt it to any secrets manager.
195
216
 
196
217
  ```ts trigger.config.ts
197
218
  import { defineConfig } from "@trigger.dev/sdk";
@@ -69,14 +69,10 @@ Now if you visit your Trigger.dev dashboard you should see the new version deplo
69
69
 
70
70
  ## Triggering deployed tasks
71
71
 
72
- Once you have deployed your tasks, you can trigger tasks exactly the same way you did locally, but with the "PROD" API key:
73
-
74
- ![Trigger.dev dashboard showing the API key](/deployment/api-key.png)
75
-
76
- Copy the API key from the dashboard and set the `TRIGGER_SECRET_KEY` environment variable, and then any tasks you trigger will run against the deployed version:
72
+ Once you have deployed your tasks, open the API Keys page for the Production environment and create a named key with **Trigger only** access. Set the key as `TRIGGER_SECRET_KEY` in your backend environment. Tasks triggered with this key run against the deployed Production version:
77
73
 
78
74
  ```txt .env
79
- TRIGGER_SECRET_KEY="tr_prod_abc123"
75
+ TRIGGER_SECRET_KEY="tr_prod_sk_abc123"
80
76
  ```
81
77
 
82
78
  Now you can trigger your tasks:
@@ -180,10 +176,10 @@ This will create an entirely new version of your tasks for the `staging` environ
180
176
 
181
177
  ![Trigger.dev dashboard showing the staging environment](/deployment/staging-deploy.png)
182
178
 
183
- Now you can trigger tasks against the staging environment by setting the `TRIGGER_SECRET_KEY` environment variable to the staging API key:
179
+ To trigger tasks against Staging, create a named key in the Staging environment with **Trigger only** access and set it as `TRIGGER_SECRET_KEY`:
184
180
 
185
181
  ```txt .env
186
- TRIGGER_SECRET_KEY="tr_stg_abcd123"
182
+ TRIGGER_SECRET_KEY="tr_stg_sk_abcd123"
187
183
  ```
188
184
 
189
185
  For additional environments beyond `prod` and `staging`, you can use [preview branches](/deployment/preview-branches), which allow you to create isolated environments for each branch of your code.
@@ -15,7 +15,7 @@ The process to use preview branches looks like this:
15
15
 
16
16
  1. Create a preview branch
17
17
  2. Deploy to the preview branch (1+ times)
18
- 3. Trigger runs using your Preview API key (`TRIGGER_SECRET_KEY`) and the branch name (`TRIGGER_PREVIEW_BRANCH`).
18
+ 3. Create a named **Trigger only** API key in the Preview environment, then trigger runs using the key (`TRIGGER_SECRET_KEY`) and branch name (`TRIGGER_PREVIEW_BRANCH`).
19
19
  4. Archive the preview branch when the branch is done.
20
20
 
21
21
  There are two main ways to do this:
@@ -41,12 +41,12 @@ For full details see our [pricing page](https://trigger.dev/pricing).
41
41
 
42
42
  ## Triggering runs and using the SDK
43
43
 
44
- Before we talk about how to deploy to preview branches, one important thing to understand is that you must set the `TRIGGER_PREVIEW_BRANCH` environment variable as well as the `TRIGGER_SECRET_KEY` environment variable.
44
+ Create a named API key in the Preview environment and set it as `TRIGGER_SECRET_KEY`. Set `TRIGGER_PREVIEW_BRANCH` to select the branch that receives the runs.
45
45
 
46
46
  When deploying to somewhere that supports `process.env` (like Node.js runtimes) you can just set the environment variables:
47
47
 
48
48
  ```bash
49
- TRIGGER_SECRET_KEY="tr_preview_1234567890"
49
+ TRIGGER_SECRET_KEY="tr_preview_sk_1234567890"
50
50
  TRIGGER_PREVIEW_BRANCH="your-branch-name"
51
51
  ```
52
52
 
@@ -57,7 +57,7 @@ import { configure } from "@trigger.dev/sdk";
57
57
  import { myTask } from "./trigger/myTasks";
58
58
 
59
59
  configure({
60
- secretKey: "tr_preview_1234567890", // WARNING: Never actually hardcode your secret key like this
60
+ secretKey: "tr_preview_sk_1234567890", // WARNING: Never actually hardcode your secret key like this
61
61
  previewBranch: "your-branch-name",
62
62
  });
63
63
 
@@ -276,6 +276,68 @@ An empty or whitespace-only value counts as "not supplied" rather than an error,
276
276
 
277
277
  Batch triggers carry the id too. `batchTrigger` resolves it per item exactly as `trigger` does, and it survives the asynchronous materialisation of batch items — so a large batch triggered during a deploy waits and releases item by item, each pinned to the deployment its calling code came from.
278
278
 
279
+ ## Chat sessions
280
+
281
+ [Chat agents](/ai-chat/overview) are covered by the same mechanism, with one difference: the id belongs to the **session**, not to a single trigger. It is resolved wherever you start the session — your server action, your route handler, `sessions.start()` — using the same order of precedence as a task trigger, and stored on the session. Every run that session goes on to schedule carries it: the first run, each continuation after an idle suspend, and each recovery after a crash.
282
+
283
+ That is what you want for a conversation. A chat started by one release of your app keeps talking to the agent build that release shipped with, however many turns and however many runs that takes.
284
+
285
+ Chats need the same two halves as tasks, and no more: a deployment carrying an id, and an app that sends the same one (explicitly, through `TRIGGER_EXTERNAL_DEPLOYMENT_ID`, or through [automatic discovery](#automatic-discovery)). There is nothing chat-specific to switch on, so an app already pinning its task runs gets pinned chats with no code change.
286
+
287
+ ```ts
288
+ // app/actions.ts
289
+ "use server";
290
+ import { chat } from "@trigger.dev/sdk/ai";
291
+ import type { myChat } from "@/trigger/chat";
292
+
293
+ // No chat-specific setup: the id is discovered per call, exactly as it is for `trigger()`.
294
+ export const startChatSession = chat.createStartSessionAction<typeof myChat>("my-chat");
295
+ ```
296
+
297
+ Three things follow from the pin living on the session:
298
+
299
+ - **Starting the session again refreshes it, and the conversation follows.** `sessions.start()` is idempotent on `chatId` and rewrites the stored config, so when your transport calls `startSession` after a redeploy, the session re-pins. A **running** agent then hands the conversation over at the next turn boundary, so the next message is answered by the deployment you just named — not several turns later when the old run happens to end. The turn already in flight finishes on the code it started on. Set [`versionSkew: "hold"`](/ai-chat/patterns/version-upgrades#staying-put) on an agent that should stay put instead.
300
+ - **There is one pin per `chatId`.** If the same conversation is open in two tabs on two different releases of your app, whichever called `startSession` most recently sets the pin for both.
301
+ - **A parked chat is waiting, not broken.** A run pinned to a deployment that hasn't landed parks, and every message sent meanwhile is stored durably and delivered once the deployment arrives. Nothing is lost — but nothing answers either, so tell the user. Re-pinning does not release a parked run: it keeps waiting for the deployment it was created for. If that deployment never lands, the run waits until its park deadline elapses, and the next message after that starts a fresh run on the session's current pin. Pass `pendingVersion` through your `startSession` callback and the transport emits a `run-pending-version` event:
302
+
303
+ ```tsx
304
+ const transport = useTriggerChatTransport({
305
+ task: "my-chat",
306
+ accessToken: ({ chatId }) => mintChatAccessToken(chatId),
307
+ startSession: ({ chatId, clientData }) => startChatSession({ chatId, clientData }),
308
+ onEvent: (event) => {
309
+ if (event.type === "run-pending-version") setDeploying(true);
310
+ if (event.type === "first-chunk") setDeploying(false);
311
+ },
312
+ });
313
+ ```
314
+
315
+ The event repeats on every message sent while the chat is parked, so a notice driven off it stays accurate. Its `source` says where the park was learned: `start` from creating the session, `send` from an append, `head-start` from the route's response header, and `upgrade` when a session followed its pin onto a deployment that hasn't landed yet — that last one arrives as soon as the handoff happens, without waiting for another message.
316
+
317
+ [Head Start](/ai-chat/fast-starts#head-start) softens this considerably: turn 1 runs in your own warm process, so a parked deployment costs nothing until step 2. The handover signal is durable, so the agent picks the turn up where it left off once the deployment lands. The transport emits `run-pending-version` with `source: "head-start"` for that case, and `chat.startHeadStart` returns `pendingVersion` for the detached flow.
318
+
319
+ ### Opting a chat out
320
+
321
+ Pass `null` and that chat is never pinned, whatever the environment says:
322
+
323
+ ```ts
324
+ export const startChatSession = chat.createStartSessionAction<typeof myChat>("my-chat", {
325
+ triggerConfig: { externalDeploymentId: null },
326
+ });
327
+ ```
328
+
329
+ Use this for a conversation that should always run on the current version — a long-lived support thread, say — while the rest of your chats stay pinned. To turn pinning off everywhere instead, set `TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION` to `0` and don't set `TRIGGER_EXTERNAL_DEPLOYMENT_ID`.
330
+
331
+ ### Escaping the pin from inside the agent
332
+
333
+ [`chat.requestUpgrade()`](/ai-chat/patterns/version-upgrades) clears the session's external deployment id as part of the handoff, so the new run is free to land on the current version. Pass a target to move to a specific deployment instead:
334
+
335
+ ```ts
336
+ chat.requestUpgrade({ externalDeploymentId: clientData.commitSha });
337
+ ```
338
+
339
+ Either way the change is persisted on the session, so the next continuation doesn't fall back to the id the agent just rejected. `lockToVersion` is a separate, explicit pin and is never cleared — `requestUpgrade()` cannot escape it, which is also why a session using it never follows its external deployment id automatically.
340
+
279
341
  ## Waiting and expiry
280
342
 
281
343
  When a run arrives with an id that isn't deployed yet, it doesn't fail — it **waits**. This is the ordinary case, not an edge case: your app frequently goes live a few seconds before your task build finishes.
@@ -34,7 +34,7 @@ This eliminates the need to manually run the `trigger.dev deploy` command or set
34
34
  <Step title="Customize build settings (optional)">
35
35
  Configure how your project is built:
36
36
 
37
- - **Trigger config file**: Path to your `trigger.config.ts` file. By default, we look for it in the root of your repository. The path should be relative to the root of your repository and contain the config file name, e.g., `apps/tasks/trigger.config.ts`.
37
+ - **Trigger config file**: Auto-detected by default — we find your `trigger.config.ts` anywhere in your repository. Set a path relative to the root of your repository to override it, e.g., `apps/tasks/trigger.config.ts`. If your repository contains more than one config file, set the path of the one to use.
38
38
  - **Install command**: Auto-detected by default, but you can override it if necessary. The command will be run from the root of your repository.
39
39
  - **Pre-build command**: Run any commands before building and deploying your project, e.g., `pnpm run prisma:generate`. The command will be run from the root of your repository.
40
40
  </Step>