@trigger.dev/sdk 4.5.0-rc.5 → 4.5.0-rc.7

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 (213) hide show
  1. package/dist/commonjs/v3/ai.d.ts +178 -5
  2. package/dist/commonjs/v3/ai.js +603 -119
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/chat-client.js +3 -0
  5. package/dist/commonjs/v3/chat-client.js.map +1 -1
  6. package/dist/commonjs/v3/chat-react.js +10 -7
  7. package/dist/commonjs/v3/chat-react.js.map +1 -1
  8. package/dist/commonjs/v3/chat-server.d.ts +8 -0
  9. package/dist/commonjs/v3/chat-server.js +32 -10
  10. package/dist/commonjs/v3/chat-server.js.map +1 -1
  11. package/dist/commonjs/v3/chat-server.test.js +51 -0
  12. package/dist/commonjs/v3/chat-server.test.js.map +1 -1
  13. package/dist/commonjs/v3/chat.js +34 -6
  14. package/dist/commonjs/v3/chat.js.map +1 -1
  15. package/dist/commonjs/v3/chat.test.js +53 -0
  16. package/dist/commonjs/v3/chat.test.js.map +1 -1
  17. package/dist/commonjs/v3/createStartSessionAction.test.js +30 -0
  18. package/dist/commonjs/v3/createStartSessionAction.test.js.map +1 -1
  19. package/dist/commonjs/v3/sessions.d.ts +11 -6
  20. package/dist/commonjs/v3/sessions.js +10 -5
  21. package/dist/commonjs/v3/sessions.js.map +1 -1
  22. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +6 -0
  23. package/dist/commonjs/v3/test/mock-chat-agent.js +1 -0
  24. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  25. package/dist/commonjs/version.js +1 -1
  26. package/dist/esm/v3/ai.d.ts +178 -5
  27. package/dist/esm/v3/ai.js +603 -120
  28. package/dist/esm/v3/ai.js.map +1 -1
  29. package/dist/esm/v3/chat-client.js +3 -0
  30. package/dist/esm/v3/chat-client.js.map +1 -1
  31. package/dist/esm/v3/chat-react.js +10 -7
  32. package/dist/esm/v3/chat-react.js.map +1 -1
  33. package/dist/esm/v3/chat-server.d.ts +8 -0
  34. package/dist/esm/v3/chat-server.js +32 -10
  35. package/dist/esm/v3/chat-server.js.map +1 -1
  36. package/dist/esm/v3/chat-server.test.js +51 -0
  37. package/dist/esm/v3/chat-server.test.js.map +1 -1
  38. package/dist/esm/v3/chat.js +34 -6
  39. package/dist/esm/v3/chat.js.map +1 -1
  40. package/dist/esm/v3/chat.test.js +53 -0
  41. package/dist/esm/v3/chat.test.js.map +1 -1
  42. package/dist/esm/v3/createStartSessionAction.test.js +30 -0
  43. package/dist/esm/v3/createStartSessionAction.test.js.map +1 -1
  44. package/dist/esm/v3/sessions.d.ts +11 -6
  45. package/dist/esm/v3/sessions.js +10 -5
  46. package/dist/esm/v3/sessions.js.map +1 -1
  47. package/dist/esm/v3/test/mock-chat-agent.d.ts +6 -0
  48. package/dist/esm/v3/test/mock-chat-agent.js +1 -0
  49. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  50. package/dist/esm/version.js +1 -1
  51. package/docs/ai/prompts.mdx +430 -0
  52. package/docs/ai-chat/actions.mdx +115 -0
  53. package/docs/ai-chat/anatomy.mdx +71 -0
  54. package/docs/ai-chat/backend.mdx +817 -0
  55. package/docs/ai-chat/background-injection.mdx +221 -0
  56. package/docs/ai-chat/changelog.mdx +850 -0
  57. package/docs/ai-chat/chat-local.mdx +174 -0
  58. package/docs/ai-chat/client-protocol.mdx +1081 -0
  59. package/docs/ai-chat/compaction.mdx +411 -0
  60. package/docs/ai-chat/custom-agents.mdx +364 -0
  61. package/docs/ai-chat/error-handling.mdx +415 -0
  62. package/docs/ai-chat/fast-starts.mdx +672 -0
  63. package/docs/ai-chat/frontend.mdx +580 -0
  64. package/docs/ai-chat/how-it-works.mdx +230 -0
  65. package/docs/ai-chat/lifecycle-hooks.mdx +530 -0
  66. package/docs/ai-chat/mcp.mdx +101 -0
  67. package/docs/ai-chat/overview.mdx +90 -0
  68. package/docs/ai-chat/patterns/branching-conversations.mdx +284 -0
  69. package/docs/ai-chat/patterns/code-sandbox.mdx +126 -0
  70. package/docs/ai-chat/patterns/database-persistence.mdx +414 -0
  71. package/docs/ai-chat/patterns/human-in-the-loop.mdx +275 -0
  72. package/docs/ai-chat/patterns/large-payloads.mdx +169 -0
  73. package/docs/ai-chat/patterns/oom-resilience.mdx +120 -0
  74. package/docs/ai-chat/patterns/persistence-and-replay.mdx +211 -0
  75. package/docs/ai-chat/patterns/recovery-boot.mdx +230 -0
  76. package/docs/ai-chat/patterns/skills.mdx +221 -0
  77. package/docs/ai-chat/patterns/sub-agents.mdx +383 -0
  78. package/docs/ai-chat/patterns/tool-result-auditing.mdx +148 -0
  79. package/docs/ai-chat/patterns/trusted-edge-signals.mdx +337 -0
  80. package/docs/ai-chat/patterns/version-upgrades.mdx +172 -0
  81. package/docs/ai-chat/pending-messages.mdx +343 -0
  82. package/docs/ai-chat/prompt-caching.mdx +206 -0
  83. package/docs/ai-chat/quick-start.mdx +161 -0
  84. package/docs/ai-chat/reference.mdx +909 -0
  85. package/docs/ai-chat/server-chat.mdx +263 -0
  86. package/docs/ai-chat/sessions.mdx +333 -0
  87. package/docs/ai-chat/testing.mdx +682 -0
  88. package/docs/ai-chat/tools.mdx +191 -0
  89. package/docs/ai-chat/types.mdx +242 -0
  90. package/docs/ai-chat/upgrade-guide.mdx +515 -0
  91. package/docs/apikeys.mdx +54 -0
  92. package/docs/building-with-ai.mdx +261 -0
  93. package/docs/bulk-actions.mdx +49 -0
  94. package/docs/changelog.mdx +6 -0
  95. package/docs/cli-deploy-commands.mdx +9 -0
  96. package/docs/cli-dev-commands.mdx +9 -0
  97. package/docs/cli-dev.mdx +8 -0
  98. package/docs/cli-init-commands.mdx +58 -0
  99. package/docs/cli-introduction.mdx +25 -0
  100. package/docs/cli-list-profiles-commands.mdx +42 -0
  101. package/docs/cli-login-commands.mdx +33 -0
  102. package/docs/cli-logout-commands.mdx +33 -0
  103. package/docs/cli-preview-archive.mdx +59 -0
  104. package/docs/cli-promote-commands.mdx +9 -0
  105. package/docs/cli-switch.mdx +43 -0
  106. package/docs/cli-update-commands.mdx +42 -0
  107. package/docs/cli-whoami-commands.mdx +33 -0
  108. package/docs/community.mdx +6 -0
  109. package/docs/config/config-file.mdx +602 -0
  110. package/docs/config/extensions/additionalFiles.mdx +38 -0
  111. package/docs/config/extensions/additionalPackages.mdx +40 -0
  112. package/docs/config/extensions/aptGet.mdx +34 -0
  113. package/docs/config/extensions/audioWaveform.mdx +20 -0
  114. package/docs/config/extensions/custom.mdx +380 -0
  115. package/docs/config/extensions/emitDecoratorMetadata.mdx +29 -0
  116. package/docs/config/extensions/esbuildPlugin.mdx +31 -0
  117. package/docs/config/extensions/ffmpeg.mdx +45 -0
  118. package/docs/config/extensions/lightpanda.mdx +56 -0
  119. package/docs/config/extensions/overview.mdx +67 -0
  120. package/docs/config/extensions/playwright.mdx +195 -0
  121. package/docs/config/extensions/prismaExtension.mdx +1014 -0
  122. package/docs/config/extensions/puppeteer.mdx +30 -0
  123. package/docs/config/extensions/pythonExtension.mdx +182 -0
  124. package/docs/config/extensions/syncEnvVars.mdx +291 -0
  125. package/docs/context.mdx +235 -0
  126. package/docs/database-connections.mdx +213 -0
  127. package/docs/deploy-environment-variables.mdx +435 -0
  128. package/docs/deployment/atomic-deployment.mdx +172 -0
  129. package/docs/deployment/overview.mdx +257 -0
  130. package/docs/deployment/preview-branches.mdx +224 -0
  131. package/docs/errors-retrying.mdx +379 -0
  132. package/docs/github-actions.mdx +222 -0
  133. package/docs/github-integration.mdx +136 -0
  134. package/docs/github-repo.mdx +8 -0
  135. package/docs/help-email.mdx +6 -0
  136. package/docs/help-slack.mdx +11 -0
  137. package/docs/hidden-tasks.mdx +56 -0
  138. package/docs/how-it-works.mdx +454 -0
  139. package/docs/how-to-reduce-your-spend.mdx +217 -0
  140. package/docs/idempotency.mdx +504 -0
  141. package/docs/introduction.mdx +223 -0
  142. package/docs/limits.mdx +241 -0
  143. package/docs/logging.mdx +195 -0
  144. package/docs/machines.mdx +952 -0
  145. package/docs/manual-setup.mdx +632 -0
  146. package/docs/mcp-agent-rules.mdx +41 -0
  147. package/docs/mcp-introduction.mdx +385 -0
  148. package/docs/mcp-tools.mdx +273 -0
  149. package/docs/migrating-from-v3.mdx +334 -0
  150. package/docs/observability/dashboards.mdx +102 -0
  151. package/docs/observability/query.mdx +585 -0
  152. package/docs/open-source-contributing.mdx +16 -0
  153. package/docs/open-source-self-hosting.mdx +541 -0
  154. package/docs/private-networking/aws-console-setup.mdx +304 -0
  155. package/docs/private-networking/overview.mdx +144 -0
  156. package/docs/private-networking/troubleshooting.mdx +78 -0
  157. package/docs/queue-concurrency.mdx +354 -0
  158. package/docs/quick-start.mdx +97 -0
  159. package/docs/realtime/auth.mdx +208 -0
  160. package/docs/realtime/backend/overview.mdx +45 -0
  161. package/docs/realtime/backend/streams.mdx +418 -0
  162. package/docs/realtime/backend/subscribe.mdx +225 -0
  163. package/docs/realtime/how-it-works.mdx +94 -0
  164. package/docs/realtime/overview.mdx +63 -0
  165. package/docs/realtime/react-hooks/overview.mdx +73 -0
  166. package/docs/realtime/react-hooks/streams.mdx +449 -0
  167. package/docs/realtime/react-hooks/subscribe.mdx +674 -0
  168. package/docs/realtime/react-hooks/swr.mdx +87 -0
  169. package/docs/realtime/react-hooks/triggering.mdx +194 -0
  170. package/docs/realtime/react-hooks/use-wait-token.mdx +34 -0
  171. package/docs/realtime/run-object.mdx +174 -0
  172. package/docs/replaying.mdx +72 -0
  173. package/docs/request-feature.mdx +6 -0
  174. package/docs/roadmap.mdx +6 -0
  175. package/docs/run-tests.mdx +20 -0
  176. package/docs/run-usage.mdx +113 -0
  177. package/docs/runs/heartbeats.mdx +38 -0
  178. package/docs/runs/max-duration.mdx +139 -0
  179. package/docs/runs/metadata.mdx +734 -0
  180. package/docs/runs/priority.mdx +31 -0
  181. package/docs/runs.mdx +396 -0
  182. package/docs/self-hosting/docker.mdx +458 -0
  183. package/docs/self-hosting/env/supervisor.mdx +74 -0
  184. package/docs/self-hosting/env/webapp.mdx +276 -0
  185. package/docs/self-hosting/kubernetes.mdx +601 -0
  186. package/docs/self-hosting/overview.mdx +108 -0
  187. package/docs/skills.mdx +85 -0
  188. package/docs/tags.mdx +120 -0
  189. package/docs/tasks/overview.mdx +697 -0
  190. package/docs/tasks/scheduled.mdx +382 -0
  191. package/docs/tasks/schemaTask.mdx +413 -0
  192. package/docs/tasks/streams.mdx +884 -0
  193. package/docs/triggering.mdx +1320 -0
  194. package/docs/troubleshooting-alerts.mdx +385 -0
  195. package/docs/troubleshooting-debugging-in-vscode.mdx +8 -0
  196. package/docs/troubleshooting-github-issues.mdx +6 -0
  197. package/docs/troubleshooting-uptime-status.mdx +6 -0
  198. package/docs/troubleshooting.mdx +398 -0
  199. package/docs/upgrading-packages.mdx +80 -0
  200. package/docs/vercel-integration.mdx +207 -0
  201. package/docs/versioning.mdx +56 -0
  202. package/docs/video-walkthrough.mdx +23 -0
  203. package/docs/wait-for-token.mdx +540 -0
  204. package/docs/wait-for.mdx +42 -0
  205. package/docs/wait-until.mdx +53 -0
  206. package/docs/wait.mdx +18 -0
  207. package/docs/writing-tasks-introduction.mdx +33 -0
  208. package/package.json +10 -6
  209. package/skills/trigger-authoring-chat-agent/SKILL.md +296 -0
  210. package/skills/trigger-authoring-tasks/SKILL.md +254 -0
  211. package/skills/trigger-chat-agent-advanced/SKILL.md +368 -0
  212. package/skills/trigger-cost-savings/SKILL.md +116 -0
  213. package/skills/trigger-realtime-and-frontend/SKILL.md +276 -0
@@ -0,0 +1,884 @@
1
+ ---
2
+ title: "Streaming data from tasks"
3
+ sidebarTitle: "Streams"
4
+ description: "Pipe continuous data from your Trigger.dev tasks to frontend or backend clients in real time. Stream AI completions, file chunks, progress updates, and more."
5
+ ---
6
+
7
+ **Streams let you pipe data from a running task to your frontend or backend as it's produced.** Think AI completions token by token, progress updates, or file chunks. You can also **send data into** running tasks with [Input Streams](#input-streams) for bidirectional flows (cancel buttons, approvals).
8
+
9
+ For subscribing to **run state changes** (status, metadata, tags) instead, see [Realtime](/realtime/overview).
10
+
11
+ <Note>
12
+ Streams require SDK version **4.1.0 or later** (`@trigger.dev/sdk` and `@trigger.dev/react-hooks`).
13
+ This doc describes the current streams behavior (v2 is the default). For pre-4.1.0 streams, see
14
+ [Pre-4.1.0 streams (legacy)](#pre-410-streams-legacy) below.
15
+ </Note>
16
+
17
+ ## Overview
18
+
19
+ Streams provide:
20
+
21
+ - **Unlimited stream length** (previously capped at 2000 chunks)
22
+ - **Unlimited active streams per run** (previously 5)
23
+ - **Improved reliability** with automatic resumption on connection loss
24
+ - **28-day stream retention** (previously 1 day)
25
+ - **Multiple client streams** can pipe to a single stream
26
+ - **Enhanced dashboard visibility** for viewing stream data in real-time
27
+
28
+ Streams v2 is the **default** when using SDK 4.1.0 or later. If you trigger tasks outside the SDK, set the `x-trigger-realtime-streams-version=v2` header. To opt out, use `auth.configure({ future: { v2RealtimeStreams: false } })` or `TRIGGER_V2_REALTIME_STREAMS=0`.
29
+
30
+ ## Limits Comparison
31
+
32
+ | Limit | Legacy (pre-4.1.0) | Current |
33
+ | -------------------------------- | ------------------ | --------- |
34
+ | Maximum stream length | 2000 | Unlimited |
35
+ | Number of active streams per run | 5 | Unlimited |
36
+ | Maximum streams per run | 10 | Unlimited |
37
+ | Maximum stream TTL | 1 day | 28 days |
38
+ | Maximum stream size | 10MB | 300 MiB |
39
+
40
+ ## Quick Start
41
+
42
+ The recommended workflow for **output** streams (data from task to client):
43
+
44
+ 1. **Define your streams** in a shared location using `streams.define()`
45
+ 2. **Use the defined stream** in your tasks with `.pipe()`, `.append()`, or `.writer()`
46
+ 3. **Read from the stream** using `.read()` or the `useRealtimeStream` hook in React
47
+
48
+ This approach gives you full type safety, better code organization, and easier maintenance as your application grows. For **input** streams (sending data into a running task), see [Input Streams](#input-streams) below.
49
+
50
+ ## Defining Typed Streams (Recommended)
51
+
52
+ The recommended way to work with streams is to define them once with `streams.define()`. This allows you to specify the chunk type and stream ID in one place, and then reuse that definition throughout your codebase with full type safety.
53
+
54
+ ### Creating a Defined Stream
55
+
56
+ Define your streams in a shared location (like `app/streams.ts` or `trigger/streams.ts`):
57
+
58
+ ```ts
59
+ import { streams, InferStreamType } from "@trigger.dev/sdk";
60
+
61
+ // Define a stream with a specific type
62
+ export const aiStream = streams.define<string>({
63
+ id: "ai-output",
64
+ });
65
+
66
+ // Export the type for use in frontend components
67
+ export type AIStreamPart = InferStreamType<typeof aiStream>;
68
+ ```
69
+
70
+ You can define streams for any JSON-serializable type:
71
+
72
+ ```ts
73
+ import { streams, InferStreamType } from "@trigger.dev/sdk";
74
+ import { UIMessageChunk } from "ai";
75
+
76
+ // Stream for AI UI message chunks
77
+ export const aiStream = streams.define<UIMessageChunk>({
78
+ id: "ai",
79
+ });
80
+
81
+ // Stream for progress updates
82
+ export const progressStream = streams.define<{ step: string; percent: number }>({
83
+ id: "progress",
84
+ });
85
+
86
+ // Stream for simple text
87
+ export const logStream = streams.define<string>({
88
+ id: "logs",
89
+ });
90
+
91
+ // Export types
92
+ export type AIStreamPart = InferStreamType<typeof aiStream>;
93
+ export type ProgressStreamPart = InferStreamType<typeof progressStream>;
94
+ export type LogStreamPart = InferStreamType<typeof logStream>;
95
+ ```
96
+
97
+ ### Using Defined Streams in Tasks
98
+
99
+ Once defined, you can use all stream methods on your defined stream:
100
+
101
+ ```ts
102
+ import { task } from "@trigger.dev/sdk";
103
+ import { aiStream } from "./streams";
104
+
105
+ export const streamTask = task({
106
+ id: "stream-task",
107
+ run: async (payload: { prompt: string }) => {
108
+ // Get a stream from an AI service, database, etc.
109
+ const stream = await getAIStream(payload.prompt);
110
+
111
+ // Pipe the stream using your defined stream
112
+ const { stream: readableStream, waitUntilComplete } = aiStream.pipe(stream);
113
+
114
+ // Option A: Iterate over the stream locally
115
+ for await (const chunk of readableStream) {
116
+ console.log("Received chunk:", chunk);
117
+ }
118
+
119
+ // Option B: Wait for the stream to complete
120
+ await waitUntilComplete();
121
+
122
+ return { message: "Stream completed" };
123
+ },
124
+ });
125
+ ```
126
+
127
+ #### Reading from a Stream
128
+
129
+ Use the defined stream's `read()` method to consume data from anywhere (frontend, backend, or another task):
130
+
131
+ ```ts
132
+ import { aiStream } from "./streams";
133
+
134
+ const stream = await aiStream.read(runId);
135
+
136
+ for await (const chunk of stream) {
137
+ console.log(chunk); // chunk is typed as the stream's chunk type
138
+ }
139
+ ```
140
+
141
+ With options:
142
+
143
+ ```ts
144
+ const stream = await aiStream.read(runId, {
145
+ timeoutInSeconds: 60, // Stop if no data for 60 seconds
146
+ startIndex: 10, // Start from the 10th chunk
147
+ });
148
+ ```
149
+
150
+ #### Appending to a Stream
151
+
152
+ Use the defined stream's `append()` method to add a single chunk:
153
+
154
+ ```ts
155
+ import { task } from "@trigger.dev/sdk";
156
+ import { aiStream, progressStream, logStream } from "./streams";
157
+
158
+ export const appendTask = task({
159
+ id: "append-task",
160
+ run: async (payload) => {
161
+ // Append to different streams with full type safety
162
+ await logStream.append("Processing started");
163
+ await progressStream.append({ step: "Initialization", percent: 0 });
164
+
165
+ // Do some work...
166
+
167
+ await progressStream.append({ step: "Processing", percent: 50 });
168
+ await logStream.append("Step 1 complete");
169
+
170
+ // Do more work...
171
+
172
+ await progressStream.append({ step: "Complete", percent: 100 });
173
+ await logStream.append("All steps complete");
174
+ },
175
+ });
176
+ ```
177
+
178
+ #### Writing Multiple Chunks
179
+
180
+ Use the defined stream's `writer()` method for more complex stream writing:
181
+
182
+ ```ts
183
+ import { task } from "@trigger.dev/sdk";
184
+ import { logStream } from "./streams";
185
+
186
+ export const writerTask = task({
187
+ id: "writer-task",
188
+ run: async (payload) => {
189
+ const { waitUntilComplete } = logStream.writer({
190
+ execute: ({ write, merge }) => {
191
+ // Write individual chunks
192
+ write("Chunk 1");
193
+ write("Chunk 2");
194
+
195
+ // Merge another stream
196
+ const additionalStream = ReadableStream.from(["Chunk 3", "Chunk 4", "Chunk 5"]);
197
+ merge(additionalStream);
198
+ },
199
+ });
200
+
201
+ await waitUntilComplete();
202
+ },
203
+ });
204
+ ```
205
+
206
+ ### Using Defined Streams in React
207
+
208
+ Defined streams work seamlessly with the `useRealtimeStream` hook:
209
+
210
+ ```tsx
211
+ "use client";
212
+
213
+ import { useRealtimeStream } from "@trigger.dev/react-hooks";
214
+ import { aiStream } from "@/app/streams";
215
+
216
+ export function StreamViewer({ accessToken, runId }: { accessToken: string; runId: string }) {
217
+ // Pass the defined stream directly - full type safety!
218
+ const { parts, error } = useRealtimeStream(aiStream, runId, {
219
+ accessToken,
220
+ timeoutInSeconds: 600,
221
+ });
222
+
223
+ if (error) return <div>Error: {error.message}</div>;
224
+ if (!parts) return <div>Loading...</div>;
225
+
226
+ return (
227
+ <div>
228
+ {parts.map((part, i) => (
229
+ <span key={i}>{part}</span>
230
+ ))}
231
+ </div>
232
+ );
233
+ }
234
+ ```
235
+
236
+ ## Direct Stream Methods (Without Defining)
237
+
238
+ <Warning>
239
+ We strongly recommend using `streams.define()` instead of direct methods. Defined streams provide
240
+ better organization, full type safety, and make it easier to maintain your codebase as it grows.
241
+ </Warning>
242
+
243
+ If you have a specific reason to avoid defined streams, you can use stream methods directly by specifying the stream key each time.
244
+
245
+ ### Direct Piping
246
+
247
+ ```ts
248
+ import { streams, task } from "@trigger.dev/sdk";
249
+
250
+ export const directStreamTask = task({
251
+ id: "direct-stream",
252
+ run: async (payload: { prompt: string }) => {
253
+ const stream = await getAIStream(payload.prompt);
254
+
255
+ // Specify the stream key directly
256
+ const { stream: readableStream, waitUntilComplete } = streams.pipe("ai-output", stream);
257
+
258
+ await waitUntilComplete();
259
+ },
260
+ });
261
+ ```
262
+
263
+ ### Direct Reading
264
+
265
+ ```ts
266
+ import { streams } from "@trigger.dev/sdk";
267
+
268
+ // Specify the stream key when reading
269
+ const stream = await streams.read(runId, "ai-output");
270
+
271
+ for await (const chunk of stream) {
272
+ console.log(chunk);
273
+ }
274
+ ```
275
+
276
+ ### Direct Appending
277
+
278
+ ```ts
279
+ import { streams, task } from "@trigger.dev/sdk";
280
+
281
+ export const directAppendTask = task({
282
+ id: "direct-append",
283
+ run: async (payload) => {
284
+ // Specify the stream key each time
285
+ await streams.append("logs", "Processing started");
286
+ await streams.append("progress", "50%");
287
+ await streams.append("logs", "Complete");
288
+ },
289
+ });
290
+ ```
291
+
292
+ ### Direct Writing
293
+
294
+ ```ts
295
+ import { streams, task } from "@trigger.dev/sdk";
296
+
297
+ export const directWriterTask = task({
298
+ id: "direct-writer",
299
+ run: async (payload) => {
300
+ const { waitUntilComplete } = streams.writer("output", {
301
+ execute: ({ write, merge }) => {
302
+ write("Chunk 1");
303
+ write("Chunk 2");
304
+ },
305
+ });
306
+
307
+ await waitUntilComplete();
308
+ },
309
+ });
310
+ ```
311
+
312
+ ## Default Stream
313
+
314
+ Every run has a "default" stream, allowing you to skip the stream key entirely. This is useful for simple cases where you only need one stream per run.
315
+
316
+ Using direct methods:
317
+
318
+ ```ts
319
+ import { streams, task } from "@trigger.dev/sdk";
320
+
321
+ export const defaultStreamTask = task({
322
+ id: "default-stream",
323
+ run: async (payload) => {
324
+ const stream = getDataStream();
325
+
326
+ // No stream key needed - uses "default"
327
+ const { waitUntilComplete } = streams.pipe(stream);
328
+
329
+ await waitUntilComplete();
330
+ },
331
+ });
332
+
333
+ // Reading from the default stream
334
+ const readStream = await streams.read(runId);
335
+ ```
336
+
337
+ ## Targeting Different Runs
338
+
339
+ You can pipe streams to parent, root, or any other run using the `target` option. This works with both defined streams and direct methods.
340
+
341
+ ### With Defined Streams
342
+
343
+ ```ts
344
+ import { task } from "@trigger.dev/sdk";
345
+ import { logStream } from "./streams";
346
+
347
+ export const childTask = task({
348
+ id: "child-task",
349
+ run: async (payload, { ctx }) => {
350
+ const stream = getDataStream();
351
+
352
+ // Pipe to parent run
353
+ logStream.pipe(stream, { target: "parent" });
354
+
355
+ // Pipe to root run
356
+ logStream.pipe(stream, { target: "root" });
357
+
358
+ // Pipe to self (default behavior)
359
+ logStream.pipe(stream, { target: "self" });
360
+
361
+ // Pipe to a specific run ID
362
+ logStream.pipe(stream, { target: payload.otherRunId });
363
+ },
364
+ });
365
+ ```
366
+
367
+ ### With Direct Methods
368
+
369
+ ```ts
370
+ import { streams, task } from "@trigger.dev/sdk";
371
+
372
+ export const childTask = task({
373
+ id: "child-task",
374
+ run: async (payload, { ctx }) => {
375
+ const stream = getDataStream();
376
+
377
+ // Pipe to parent run
378
+ streams.pipe("output", stream, { target: "parent" });
379
+
380
+ // Pipe to root run
381
+ streams.pipe("output", stream, { target: "root" });
382
+
383
+ // Pipe to a specific run ID
384
+ streams.pipe("output", stream, { target: payload.otherRunId });
385
+ },
386
+ });
387
+ ```
388
+
389
+ ## Streaming from Outside a Task
390
+
391
+ If you specify a `target` run ID, you can pipe streams from anywhere (like a Next.js API route):
392
+
393
+ ```ts
394
+ import { streams } from "@trigger.dev/sdk";
395
+ import { openai } from "@ai-sdk/openai";
396
+ import { streamText } from "ai";
397
+
398
+ export async function POST(req: Request) {
399
+ const { messages, runId } = await req.json();
400
+
401
+ const result = streamText({
402
+ model: openai("gpt-4o"),
403
+ messages,
404
+ });
405
+
406
+ // Pipe AI stream to a Trigger.dev run
407
+ const { stream } = streams.pipe("ai-stream", result.toUIMessageStream(), {
408
+ target: runId,
409
+ });
410
+
411
+ return new Response(stream as any, {
412
+ headers: { "Content-Type": "text/event-stream" },
413
+ });
414
+ }
415
+ ```
416
+
417
+ ## React Hook
418
+
419
+ Use the `useRealtimeStream` hook to subscribe to streams in your React components.
420
+
421
+ ### With Defined Streams (Recommended)
422
+
423
+ ```tsx
424
+ "use client";
425
+
426
+ import { useRealtimeStream } from "@trigger.dev/react-hooks";
427
+ import { aiStream } from "@/app/streams";
428
+
429
+ export function StreamViewer({ accessToken, runId }: { accessToken: string; runId: string }) {
430
+ // Pass the defined stream directly for full type safety
431
+ const { parts, error } = useRealtimeStream(aiStream, runId, {
432
+ accessToken,
433
+ timeoutInSeconds: 600,
434
+ onData: (chunk) => {
435
+ console.log("New chunk:", chunk); // chunk is typed!
436
+ },
437
+ });
438
+
439
+ if (error) return <div>Error: {error.message}</div>;
440
+ if (!parts) return <div>Loading...</div>;
441
+
442
+ return (
443
+ <div>
444
+ {parts.map((part, i) => (
445
+ <span key={i}>{part}</span>
446
+ ))}
447
+ </div>
448
+ );
449
+ }
450
+ ```
451
+
452
+ ### With Direct Stream Keys
453
+
454
+ If you prefer not to use defined streams, you can specify the stream key directly:
455
+
456
+ ```tsx
457
+ "use client";
458
+
459
+ import { useRealtimeStream } from "@trigger.dev/react-hooks";
460
+
461
+ export function StreamViewer({ accessToken, runId }: { accessToken: string; runId: string }) {
462
+ const { parts, error } = useRealtimeStream<string>(runId, "ai-output", {
463
+ accessToken,
464
+ timeoutInSeconds: 600,
465
+ });
466
+
467
+ if (error) return <div>Error: {error.message}</div>;
468
+ if (!parts) return <div>Loading...</div>;
469
+
470
+ return (
471
+ <div>
472
+ {parts.map((part, i) => (
473
+ <span key={i}>{part}</span>
474
+ ))}
475
+ </div>
476
+ );
477
+ }
478
+ ```
479
+
480
+ ### Using Default Stream
481
+
482
+ ```tsx
483
+ // Omit stream key to use the default stream
484
+ const { parts, error } = useRealtimeStream<string>(runId, {
485
+ accessToken,
486
+ });
487
+ ```
488
+
489
+ ### Hook Options
490
+
491
+ ```tsx
492
+ const { parts, error } = useRealtimeStream(streamDef, runId, {
493
+ accessToken: "pk_...", // Required: Public access token
494
+ baseURL: "https://api.trigger.dev", // Optional: Custom API URL
495
+ timeoutInSeconds: 60, // Optional: Timeout (default: 60)
496
+ startIndex: 0, // Optional: Start from specific chunk
497
+ throttleInMs: 16, // Optional: Throttle updates (default: 16ms)
498
+ onData: (chunk) => {}, // Optional: Callback for each chunk
499
+ });
500
+ ```
501
+
502
+ ## Input Streams
503
+
504
+ Input Streams let you send data **into** a running task from your backend or frontend. While output streams (above) send data out of tasks, input streams complete the loop — enabling bidirectional communication.
505
+
506
+ <Note>
507
+ Input Streams require SDK version **4.4.2 or later** and use the same streams infrastructure (v2 is the default). If you're on an older SDK, calling `.on()` or `.once()` will throw with instructions to enable v2 streams. See [Pre-4.1.0 streams (legacy)](#pre-410-streams-legacy) for the older metadata-based API.
508
+ </Note>
509
+
510
+ ### Input Streams overview
511
+
512
+ Input Streams solve three common problems:
513
+
514
+ - **Cancelling AI streams mid-generation.** When you use AI SDK's `streamText` inside a task, the LLM keeps generating until it's done — even if the user clicked "Stop." With input streams, your frontend sends a cancel signal and the task aborts the LLM call immediately.
515
+ - **Human-in-the-loop workflows.** A task generates a draft, then pauses and waits for the user to approve or edit it before continuing.
516
+ - **Interactive agents.** An AI agent running as a task needs follow-up information from the user mid-execution — clarifying a question, choosing between options, or providing additional context.
517
+
518
+ ### Quick Start (Input Streams)
519
+
520
+ 1. **Define** input streams in a shared file with `streams.input<T>({ id: "..." })`.
521
+ 2. **Receive** in your task with `.wait()`, `.once()`, `.on()`, or `.peek()`.
522
+ 3. **Send** from your backend with `.send(runId, data)` or from the frontend with the `useInputStreamSend` hook (see [Realtime React hooks](/realtime/react-hooks/streams#useinputstreamsend)).
523
+
524
+ ### Defining Input Streams
525
+
526
+ Use `streams.input()` to define a typed input stream. The generic parameter controls the shape of data that can be sent:
527
+
528
+ ```ts
529
+ import { streams } from "@trigger.dev/sdk";
530
+
531
+ export const cancelSignal = streams.input<{ reason?: string }>({
532
+ id: "cancel",
533
+ });
534
+
535
+ export const approval = streams.input<{ approved: boolean; reviewer: string }>({
536
+ id: "approval",
537
+ });
538
+
539
+ export const userResponse = streams.input<{
540
+ action: "approve" | "reject" | "edit";
541
+ message?: string;
542
+ edits?: Record<string, string>;
543
+ }>({
544
+ id: "user-response",
545
+ });
546
+ ```
547
+
548
+ Type safety is enforced through the generic — both `.send()` and the receiving methods (`.wait()`, `.once()`, `.on()`, `.peek()`) share the same type.
549
+
550
+ ### Receiving data inside a task
551
+
552
+ | Method | Task suspended? | Compute cost while waiting | Best for |
553
+ |--------|-----------------|----------------------------|-----------|
554
+ | `.wait()` | **Yes** | **None** — process freed | Approval gates, human-in-the-loop, long waits |
555
+ | `.once()` | No | Full — process stays alive | Short waits, concurrent work; returns result object with `.unwrap()` |
556
+ | `.on(handler)` | No | Full — process stays alive | Continuous listening (cancel signals, live updates) |
557
+ | `.peek()` | No | None | Non-blocking check for latest buffered value |
558
+
559
+ #### `wait()` — Suspend until data arrives
560
+
561
+ Suspends the task entirely, freeing compute resources. The task resumes when data arrives via `.send()`. Returns a [`ManualWaitpointPromise`](/wait-for-token) — the same type as `wait.forToken()`.
562
+
563
+ ```ts
564
+ import { task } from "@trigger.dev/sdk";
565
+ import { approval } from "./streams";
566
+
567
+ export const publishPost = task({
568
+ id: "publish-post",
569
+ run: async (payload: { postId: string }) => {
570
+ const draft = await prepareDraft(payload.postId);
571
+ await notifyReviewer(draft);
572
+
573
+ const result = await approval.wait({ timeout: "7d" });
574
+
575
+ if (result.ok) {
576
+ if (result.output.approved) {
577
+ await publish(draft);
578
+ return { published: true, reviewer: result.output.reviewer };
579
+ }
580
+ return { published: false, reviewer: result.output.reviewer };
581
+ }
582
+ return { published: false, timedOut: true };
583
+ },
584
+ });
585
+ ```
586
+
587
+ Use `.unwrap()` to throw on timeout: `const data = await approval.wait({ timeout: "24h" }).unwrap();`
588
+
589
+ **Options:** `timeout` (e.g. `"30s"`, `"5m"`, `"24h"`, `"7d"`), `idempotencyKey`, `idempotencyKeyTTL`, `tags`. Use `idempotencyKey` when your task has retries so the same waitpoint is resumed across retries.
590
+
591
+ #### `once()` — Wait for the next value (non-suspending)
592
+
593
+ Blocks until data arrives but keeps the task process alive. Returns a result object; use `.unwrap()` to get the data or throw on timeout.
594
+
595
+ ```ts
596
+ const result = await approval.once({ timeoutMs: 300_000 });
597
+ if (result.ok) {
598
+ console.log(result.output.approved);
599
+ }
600
+ // Or: const data = await approval.once({ timeoutMs: 300_000 }).unwrap();
601
+ ```
602
+
603
+ `once()` also accepts a `signal` (e.g. `AbortController.signal`) for cancellation.
604
+
605
+ #### `on()` — Listen for every value
606
+
607
+ Registers a persistent handler that fires on every piece of data. Handlers are automatically cleaned up when the task run completes. Call `.off()` on the returned subscription to stop listening early.
608
+
609
+ ```ts
610
+ const controller = new AbortController();
611
+ cancelSignal.on((data) => {
612
+ console.log("Cancelled:", data.reason);
613
+ controller.abort();
614
+ });
615
+ const result = streamText({ ..., abortSignal: controller.signal });
616
+ ```
617
+
618
+ #### `peek()` — Non-blocking check
619
+
620
+ Returns the most recent buffered value without waiting, or `undefined` if nothing has been received yet.
621
+
622
+ ```ts
623
+ const latest = cancelSignal.peek();
624
+ if (latest) {
625
+ // A cancel was already sent before we checked
626
+ }
627
+ ```
628
+
629
+ ### Sending data to a running task
630
+
631
+ Use `.send(runId, data)` from your backend to push data into a running task. See the [backend input streams guide](/realtime/backend/input-streams) for API route patterns.
632
+
633
+ ```ts
634
+ import { cancelSignal, approval } from "./trigger/streams";
635
+
636
+ await cancelSignal.send(runId, { reason: "User clicked stop" });
637
+ await approval.send(runId, { approved: true, reviewer: "alice@example.com" });
638
+ ```
639
+
640
+ ### Complete example: Cancellable AI streaming
641
+
642
+ Stream an AI response while allowing the user to cancel mid-generation.
643
+
644
+ **Define the streams:**
645
+
646
+ ```ts
647
+ import { streams } from "@trigger.dev/sdk";
648
+
649
+ export const aiOutput = streams.define<string>({ id: "ai" });
650
+ export const cancelStream = streams.input<{ reason?: string }>({ id: "cancel" });
651
+ ```
652
+
653
+ **Task:** Register `cancelStream.on()` to abort an `AbortController`, then pipe `streamText(...).textStream` to `aiOutput`. **Backend:** POST to an API route that calls `cancelStream.send(runId, { reason: "User clicked stop" })`. **Frontend:** Use `useRealtimeStream(aiOutput, runId, { accessToken })` and a button that calls your cancel API (or use the `useInputStreamSend` hook; see [Realtime React hooks](/realtime/react-hooks/streams#useinputstreamsend)).
654
+
655
+ **Important notes (input streams):** You cannot send to a completed, failed, or canceled run. Max payload per `.send()` is 1MB. Data sent before a listener is registered is buffered and delivered when a listener attaches; `.wait()` handles the buffering race automatically. Use `.wait()` for long waits to free compute; use `.once()` for short waits or concurrent work. Define input streams in a shared location and combine with output streams for full bidirectional communication.
656
+
657
+ ## Complete Example: AI Streaming
658
+
659
+ ### Define the stream
660
+
661
+ ```ts
662
+ // app/streams.ts
663
+ import { streams, InferStreamType } from "@trigger.dev/sdk";
664
+ import { UIMessageChunk } from "ai";
665
+
666
+ export const aiStream = streams.define<UIMessageChunk>({
667
+ id: "ai",
668
+ });
669
+
670
+ export type AIStreamPart = InferStreamType<typeof aiStream>;
671
+ ```
672
+
673
+ ### Create the task
674
+
675
+ ```ts
676
+ // trigger/ai-task.ts
677
+ import { task } from "@trigger.dev/sdk";
678
+ import { openai } from "@ai-sdk/openai";
679
+ import { streamText } from "ai";
680
+ import { aiStream } from "@/app/streams";
681
+
682
+ export const generateAI = task({
683
+ id: "generate-ai",
684
+ run: async (payload: { prompt: string }) => {
685
+ const result = streamText({
686
+ model: openai("gpt-4o"),
687
+ prompt: payload.prompt,
688
+ });
689
+
690
+ const { waitUntilComplete } = aiStream.pipe(result.toUIMessageStream());
691
+
692
+ await waitUntilComplete();
693
+
694
+ return { success: true };
695
+ },
696
+ });
697
+ ```
698
+
699
+ ### Frontend component
700
+
701
+ ```tsx
702
+ // components/ai-stream.tsx
703
+ "use client";
704
+
705
+ import { useRealtimeStream } from "@trigger.dev/react-hooks";
706
+ import { aiStream } from "@/app/streams";
707
+
708
+ export function AIStream({ accessToken, runId }: { accessToken: string; runId: string }) {
709
+ const { parts, error } = useRealtimeStream(aiStream, runId, {
710
+ accessToken,
711
+ timeoutInSeconds: 300,
712
+ });
713
+
714
+ if (error) return <div>Error: {error.message}</div>;
715
+ if (!parts) return <div>Loading...</div>;
716
+
717
+ return (
718
+ <div className="prose">
719
+ {parts.map((part, i) => (
720
+ <span key={i}>{part}</span>
721
+ ))}
722
+ </div>
723
+ );
724
+ }
725
+ ```
726
+
727
+ ## Migration from v1
728
+
729
+ If you're using the old `metadata.stream()` API, here's how to migrate to the recommended v2 approach:
730
+
731
+ ### Step 1: Define Your Streams
732
+
733
+ Create a shared streams definition file:
734
+
735
+ ```ts
736
+ // app/streams.ts or trigger/streams.ts
737
+ import { streams, InferStreamType } from "@trigger.dev/sdk";
738
+
739
+ export const myStream = streams.define<string>({
740
+ id: "my-stream",
741
+ });
742
+
743
+ export type MyStreamPart = InferStreamType<typeof myStream>;
744
+ ```
745
+
746
+ ### Step 2: Update Your Tasks
747
+
748
+ Replace `metadata.stream()` with the defined stream's `pipe()` method:
749
+
750
+ ```ts
751
+ // Before (v1)
752
+ import { metadata, task } from "@trigger.dev/sdk";
753
+
754
+ export const myTask = task({
755
+ id: "my-task",
756
+ run: async (payload) => {
757
+ const stream = getDataStream();
758
+ await metadata.stream("my-stream", stream);
759
+ },
760
+ });
761
+ ```
762
+
763
+ ```ts
764
+ // After (v2 - Recommended)
765
+ import { task } from "@trigger.dev/sdk";
766
+ import { myStream } from "./streams";
767
+
768
+ export const myTask = task({
769
+ id: "my-task",
770
+ run: async (payload) => {
771
+ const stream = getDataStream();
772
+
773
+ // Don't await - returns immediately
774
+ const { waitUntilComplete } = myStream.pipe(stream);
775
+
776
+ // Optionally wait for completion
777
+ await waitUntilComplete();
778
+ },
779
+ });
780
+ ```
781
+
782
+ ### Step 3: Update Your Frontend
783
+
784
+ Use the defined stream with `useRealtimeStream`:
785
+
786
+ ```tsx
787
+ // Before
788
+ const { parts, error } = useRealtimeStream<string>(runId, "my-stream", {
789
+ accessToken,
790
+ });
791
+ ```
792
+
793
+ ```tsx
794
+ // After
795
+ import { myStream } from "@/app/streams";
796
+
797
+ const { parts, error } = useRealtimeStream(myStream, runId, {
798
+ accessToken,
799
+ });
800
+ ```
801
+
802
+ ### Alternative: Direct Methods (Not Recommended)
803
+
804
+ If you prefer not to use defined streams, you can use direct methods:
805
+
806
+ ```ts
807
+ import { streams, task } from "@trigger.dev/sdk";
808
+
809
+ export const myTask = task({
810
+ id: "my-task",
811
+ run: async (payload) => {
812
+ const stream = getDataStream();
813
+ const { waitUntilComplete } = streams.pipe("my-stream", stream);
814
+ await waitUntilComplete();
815
+ },
816
+ });
817
+ ```
818
+
819
+ ## Reliability Features
820
+
821
+ Streams v2 includes automatic reliability improvements:
822
+
823
+ - **Automatic resumption**: If a connection is lost, both appending and reading will automatically resume from the last successful chunk
824
+ - **No data loss**: Network issues won't cause stream data to be lost
825
+ - **Idempotent operations**: Duplicate chunks are automatically handled
826
+
827
+ These improvements happen automatically - no code changes needed.
828
+
829
+ ## Dashboard Integration
830
+
831
+ Streams are now visible in the Trigger.dev dashboard, allowing you to:
832
+
833
+ - View stream data in real-time as it's generated
834
+ - Inspect historical stream data for completed runs
835
+ - Debug streaming issues with full visibility into chunk delivery
836
+
837
+ <video src="https://content.trigger.dev/streams-v2-dashboard.mp4" controls muted autoPlay loop />
838
+
839
+ ## Best Practices
840
+
841
+ 1. **Always use `streams.define()`**: Define your streams in a shared location for better organization, type safety, and code reusability. This is the recommended approach for all streams.
842
+ 2. **Export stream types**: Use `InferStreamType` to export types for your frontend components
843
+ 3. **Handle errors gracefully**: Always check for errors when reading streams in your UI
844
+ 4. **Set appropriate timeouts**: Adjust `timeoutInSeconds` based on your use case (AI completions may need longer timeouts)
845
+ 5. **Target parent runs**: When orchestrating with child tasks, pipe to parent runs for easier consumption
846
+ 6. **Throttle frontend updates**: Use `throttleInMs` in `useRealtimeStream` to prevent excessive re-renders
847
+ 7. **Use descriptive stream IDs**: Choose clear, descriptive IDs like `"ai-output"` or `"progress"` instead of generic names
848
+
849
+ ## Pre-4.1.0 streams (legacy)
850
+
851
+ Prior to SDK 4.1.0, streams used the older metadata-based API. If you're on an earlier version, see [metadata.stream()](/runs/metadata#stream) for legacy usage. With 4.4.2+, [Input Streams](#input-streams) are available and documented in this page.
852
+
853
+ ## Troubleshooting
854
+
855
+ ### Stream not appearing in dashboard
856
+
857
+ - Verify your task is actually writing to the stream
858
+ - Check that the stream key matches between writing and reading
859
+
860
+ ### Stream timeout errors
861
+
862
+ - Increase `timeoutInSeconds` in your `read()` or `useRealtimeStream()` calls
863
+ - Ensure your stream source is actively producing data
864
+ - Check network connectivity between your application and Trigger.dev
865
+
866
+ ### Missing chunks
867
+
868
+ - With the current streams implementation, chunks should not be lost due to automatic resumption
869
+ - Verify you're reading from the correct stream key
870
+ - Check the `startIndex` option if you're not seeing expected chunks
871
+
872
+ ### Input streams not working
873
+
874
+ - Input streams require SDK **4.4.2 or later** and the default streams (v2) infrastructure. Ensure you're on a recent SDK and not using the legacy metadata.stream() API.
875
+ - If `.on()` or `.once()` throw, follow the error message to enable v2 streams (they are default in 4.1.0+).
876
+
877
+ ### "Stream is being deleted" during long waits
878
+
879
+ If a stream is created but stays empty for ~1 hour (for example, during a long `wait.forToken()` or `wait.for()`), the streams backend may garbage-collect it. When the run resumes and tries to use the stream, you'll see `S2Error: Stream is being deleted` and the task retries from the beginning.
880
+
881
+ Two ways to avoid this:
882
+
883
+ - Close the stream before the wait and open a new one when the run resumes.
884
+ - Write a heartbeat record to the stream every 20–30 minutes during the wait so it's never empty long enough to be deleted.