@trigger.dev/sdk 4.6.4 → 4.7.1

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 (123) hide show
  1. package/dist/commonjs/v3/ai.d.ts +5 -2
  2. package/dist/commonjs/v3/ai.js +189 -266
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/chat-client.js +7 -0
  5. package/dist/commonjs/v3/chat-client.js.map +1 -1
  6. package/dist/commonjs/v3/chat-server.d.ts +1 -0
  7. package/dist/commonjs/v3/chat-server.js +8 -0
  8. package/dist/commonjs/v3/chat-server.js.map +1 -1
  9. package/dist/commonjs/v3/chat.d.ts +28 -4
  10. package/dist/commonjs/v3/chat.js +41 -9
  11. package/dist/commonjs/v3/chat.js.map +1 -1
  12. package/dist/commonjs/v3/chatRouteWait.d.ts +21 -0
  13. package/dist/commonjs/v3/chatRouteWait.js +43 -0
  14. package/dist/commonjs/v3/chatRouteWait.js.map +1 -0
  15. package/dist/commonjs/v3/compactionResponse.js +5 -0
  16. package/dist/commonjs/v3/compactionResponse.js.map +1 -1
  17. package/dist/commonjs/v3/concurrency-shared.d.ts +13 -0
  18. package/dist/commonjs/v3/concurrency-shared.js +35 -0
  19. package/dist/commonjs/v3/concurrency-shared.js.map +1 -0
  20. package/dist/commonjs/v3/concurrencyLimits.d.ts +73 -0
  21. package/dist/commonjs/v3/concurrencyLimits.js +166 -0
  22. package/dist/commonjs/v3/concurrencyLimits.js.map +1 -0
  23. package/dist/commonjs/v3/index.d.ts +2 -1
  24. package/dist/commonjs/v3/index.js +3 -1
  25. package/dist/commonjs/v3/index.js.map +1 -1
  26. package/dist/commonjs/v3/managedChatResponse.d.ts +44 -0
  27. package/dist/commonjs/v3/managedChatResponse.js +233 -0
  28. package/dist/commonjs/v3/managedChatResponse.js.map +1 -0
  29. package/dist/commonjs/v3/queues.d.ts +31 -0
  30. package/dist/commonjs/v3/queues.js +31 -0
  31. package/dist/commonjs/v3/queues.js.map +1 -1
  32. package/dist/commonjs/v3/shared.d.ts +18 -1
  33. package/dist/commonjs/v3/shared.js +137 -47
  34. package/dist/commonjs/v3/shared.js.map +1 -1
  35. package/dist/commonjs/v3/steeringContext.d.ts +41 -0
  36. package/dist/commonjs/v3/steeringContext.js +118 -0
  37. package/dist/commonjs/v3/steeringContext.js.map +1 -0
  38. package/dist/commonjs/v3/transcriptStorage.d.ts +4 -1
  39. package/dist/commonjs/v3/transcriptStorage.js +51 -4
  40. package/dist/commonjs/v3/transcriptStorage.js.map +1 -1
  41. package/dist/commonjs/version.js +1 -1
  42. package/dist/esm/v3/ai.d.ts +5 -2
  43. package/dist/esm/v3/ai.js +189 -266
  44. package/dist/esm/v3/ai.js.map +1 -1
  45. package/dist/esm/v3/chat-client.js +7 -0
  46. package/dist/esm/v3/chat-client.js.map +1 -1
  47. package/dist/esm/v3/chat-server.d.ts +1 -0
  48. package/dist/esm/v3/chat-server.js +8 -0
  49. package/dist/esm/v3/chat-server.js.map +1 -1
  50. package/dist/esm/v3/chat.d.ts +28 -4
  51. package/dist/esm/v3/chat.js +41 -9
  52. package/dist/esm/v3/chat.js.map +1 -1
  53. package/dist/esm/v3/chatRouteWait.d.ts +21 -0
  54. package/dist/esm/v3/chatRouteWait.js +40 -0
  55. package/dist/esm/v3/chatRouteWait.js.map +1 -0
  56. package/dist/esm/v3/compactionResponse.js +5 -0
  57. package/dist/esm/v3/compactionResponse.js.map +1 -1
  58. package/dist/esm/v3/concurrency-shared.d.ts +13 -0
  59. package/dist/esm/v3/concurrency-shared.js +31 -0
  60. package/dist/esm/v3/concurrency-shared.js.map +1 -0
  61. package/dist/esm/v3/concurrencyLimits.d.ts +73 -0
  62. package/dist/esm/v3/concurrencyLimits.js +158 -0
  63. package/dist/esm/v3/concurrencyLimits.js.map +1 -0
  64. package/dist/esm/v3/index.d.ts +2 -1
  65. package/dist/esm/v3/index.js +2 -1
  66. package/dist/esm/v3/index.js.map +1 -1
  67. package/dist/esm/v3/managedChatResponse.d.ts +44 -0
  68. package/dist/esm/v3/managedChatResponse.js +228 -0
  69. package/dist/esm/v3/managedChatResponse.js.map +1 -0
  70. package/dist/esm/v3/queues.d.ts +31 -0
  71. package/dist/esm/v3/queues.js +31 -0
  72. package/dist/esm/v3/queues.js.map +1 -1
  73. package/dist/esm/v3/shared.d.ts +18 -1
  74. package/dist/esm/v3/shared.js +136 -47
  75. package/dist/esm/v3/shared.js.map +1 -1
  76. package/dist/esm/v3/steeringContext.d.ts +41 -0
  77. package/dist/esm/v3/steeringContext.js +113 -0
  78. package/dist/esm/v3/steeringContext.js.map +1 -0
  79. package/dist/esm/v3/transcriptStorage.d.ts +4 -1
  80. package/dist/esm/v3/transcriptStorage.js +51 -4
  81. package/dist/esm/v3/transcriptStorage.js.map +1 -1
  82. package/dist/esm/version.js +1 -1
  83. package/docs/ai-chat/client-protocol.mdx +3 -1
  84. package/docs/ai-chat/error-handling.mdx +44 -76
  85. package/docs/ai-chat/fast-starts.mdx +1 -1
  86. package/docs/ai-chat/frontend.mdx +27 -21
  87. package/docs/ai-chat/patterns/branching-conversations.mdx +95 -230
  88. package/docs/ai-chat/patterns/human-in-the-loop.mdx +166 -164
  89. package/docs/ai-chat/patterns/tool-result-auditing.mdx +28 -27
  90. package/docs/ai-chat/patterns/version-upgrades.mdx +4 -4
  91. package/docs/ai-chat/pending-messages.mdx +19 -5
  92. package/docs/ai-chat/quick-start.mdx +26 -20
  93. package/docs/ai-chat/reference.mdx +21 -3
  94. package/docs/ai-chat/sessions.mdx +1 -1
  95. package/docs/ai-chat/testing.mdx +16 -4
  96. package/docs/concurrency.mdx +384 -0
  97. package/docs/config/config-file.mdx +2 -0
  98. package/docs/database-connections.mdx +3 -3
  99. package/docs/deploy-environment-variables.mdx +6 -0
  100. package/docs/deployment/atomic-deployment.mdx +416 -132
  101. package/docs/deployment/overview.mdx +2 -2
  102. package/docs/deployment/preview-branches.mdx +2 -2
  103. package/docs/github-actions.mdx +26 -16
  104. package/docs/github-integration.mdx +2 -2
  105. package/docs/idempotency.mdx +43 -5
  106. package/docs/introduction.mdx +1 -1
  107. package/docs/limits.mdx +16 -6
  108. package/docs/manual-setup.mdx +1 -1
  109. package/docs/observability/query.mdx +25 -0
  110. package/docs/queues.mdx +271 -0
  111. package/docs/reports.mdx +1 -1
  112. package/docs/runs/priority.mdx +2 -25
  113. package/docs/self-hosting/env/webapp.mdx +9 -0
  114. package/docs/self-hosting/kubernetes.mdx +14 -3
  115. package/docs/tasks/overview.mdx +3 -5
  116. package/docs/troubleshooting-alerts.mdx +124 -1
  117. package/docs/troubleshooting.mdx +12 -0
  118. package/docs/vercel-integration.mdx +6 -7
  119. package/docs/versioning.mdx +1 -1
  120. package/docs/writing-tasks-introduction.mdx +2 -1
  121. package/package.json +2 -2
  122. package/docs/deployment/version-skew-protection.mdx +0 -492
  123. package/docs/queue-concurrency.mdx +0 -358
@@ -1,184 +1,468 @@
1
1
  ---
2
- title: "Atomic deploys"
3
- sidebarTitle: "Atomic deploys"
4
- description: "Use atomic deploys to coordinate changes to your tasks and your application."
2
+ title: "Atomic deployments"
3
+ sidebarTitle: "Atomic deployments"
4
+ description: "Keep your app and your tasks in sync with version skew protection: pin every run to the deployment built for the release of your app that triggered it."
5
5
  ---
6
6
 
7
+ **Atomic deployments are provided by version skew protection: every run is pinned to the deployment built for the release of your app that triggered it.**
8
+
9
+ Your app and your tasks are deployed separately, so they are never live at exactly the same instant. In the window between them, an app running new code can trigger tasks built from old code — or the other way around. That's **version skew**.
10
+
11
+ Version skew protection closes the window. You give a deployment an **external deployment id**, your app sends the same id when it triggers, and Trigger.dev pins the run to the deployment carrying that id.
12
+
13
+ <Note>
14
+ Version skew protection is available from `@trigger.dev/sdk` and the `trigger.dev` CLI
15
+ [v4.5.12](https://github.com/triggerdotdev/trigger.dev/releases/tag/v4.5.12), the release that
16
+ introduced external deployment ids. Upgrade both with `npx trigger.dev@latest update`. On an older
17
+ version no id is sent, and runs execute on the current version with no warning.
18
+ </Note>
19
+
20
+ ## The guarantee
21
+
22
+ Once your app is sending an external deployment id, a triggered run has one of four outcomes:
23
+
24
+ | Situation | What happens |
25
+ | ---------------------------------------------------- | ------------------------------------------------------------------- |
26
+ | A deployment with that id is live | The run is **pinned** to it and executes immediately. |
27
+ | A deployment with that id is still building | The run **waits**, then executes pinned to it once the build lands. |
28
+ | No deployment with that id ever arrives | The run waits up to **1 hour**, then expires. |
29
+ | No id was sent at all | Nothing changes — the run executes on the current version. |
30
+
31
+ The common case is your app going live a few seconds before your task build finishes. The run waits for the matching deployment instead of executing on the previous one:
32
+
33
+ ```mermaid
34
+ sequenceDiagram
35
+ participant CI as Your CI
36
+ participant App as Your app (release abc123)
37
+ participant T as Trigger.dev
38
+ participant D as Deployment abc123
39
+
40
+ CI->>T: trigger deploy --external-id abc123
41
+ T->>D: Start building
42
+ CI->>App: Deploy app with TRIGGER_EXTERNAL_DEPLOYMENT_ID=abc123
43
+ App->>T: myTask.trigger(payload) carrying id abc123
44
+ Note over T: No deployment with abc123 is live yet<br/>Run status: Pending version
45
+ D-->>T: Build finishes, deployment is live
46
+ T->>D: Release the run, pinned to abc123
47
+ D-->>T: Run completes
48
+ ```
49
+
50
+ **The id is the contract.** There is no dashboard setting and nothing to switch on: if a trigger carries an external deployment id, that id is honoured. If it doesn't, behaviour is exactly what it is today.
51
+
52
+ This works identically in production, staging and preview (including per-branch preview deployments), with no per-environment configuration.
53
+
54
+ ## Quick start
55
+
56
+ The id is free-form and opaque to Trigger.dev — a commit SHA, a release tag, a CI run id, anything up to **128 characters**. The only rule is that both halves carry the **same value**.
57
+
58
+ <Steps>
59
+
60
+ <Step title="Deploy your tasks with an external id">
61
+
62
+ ```bash
63
+ npx trigger.dev@latest deploy --external-id "$(git rev-parse HEAD)"
64
+ ```
65
+
66
+ </Step>
67
+
68
+ <Step title="Give your running app the same value">
69
+
70
+ Set this in your hosting platform's runtime environment variables, alongside `TRIGGER_SECRET_KEY`:
71
+
72
+ ```bash
73
+ TRIGGER_EXTERNAL_DEPLOYMENT_ID=<the-same-commit-sha>
74
+ ```
75
+
76
+ </Step>
77
+
78
+ </Steps>
79
+
7
80
  <Warning>
8
- **There's now a simpler way to do this.** [Version skew
9
- protection](/deployment/version-skew-protection) solves the same problem without a second
10
- deployment, without gating your app's deploy, and without setting `TRIGGER_VERSION` — and it covers
11
- staging and preview as well as production. If you use the [Vercel
12
- integration](/vercel-integration), its **automatic atomic deployments** setting is now deprecated
13
- in favour of skew protection.
14
-
15
- The manual workflows on this page still work, and remain the right answer if you specifically want
16
- your application's deployment held back until your tasks have finished building.
81
+ Whatever you use on both sides, make sure it can't expand to an empty string. An unset shell
82
+ variable deploys with no id at all, which silently gives you no protection rather than an error.
17
83
  </Warning>
18
84
 
19
- Atomic deploys in Trigger.dev allow you to synchronize the deployment of your application with a specific version of your tasks. This ensures that your application always uses the correct version of its associated tasks, preventing inconsistencies or errors due to version mismatches.
85
+ That's it. Every task triggered by that release of your app now runs on the deployment you built for it.
86
+
87
+ This recipe works on **every platform**, needs no integration, and is not git-specific. If you deploy to Vercel with the [Vercel integration](/vercel-integration), both halves are filled in for you — see [Automatic skew protection on Vercel](#automatic-skew-protection-on-vercel).
88
+
89
+ ## Deploying with an external id
90
+
91
+ ```bash
92
+ npx trigger.dev@latest deploy --external-id <value>
93
+ ```
94
+
95
+ The value is stored on the deployment. It is never interpreted, parsed or validated beyond its length — the only constraint is **128 characters or fewer**.
96
+
97
+ ### Reusing an id
98
+
99
+ Because deploys are often retried, redeployed and re-run by CI, repeating an id is a normal thing to do rather than an error. What happens depends on the state of the deployment that already holds it:
100
+
101
+ | Existing deployment for this id | Default behaviour | With `--force` |
102
+ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------ |
103
+ | **Deployed** | **No build.** The CLI reports the existing version, sets the same outputs, and exits successfully. | Builds a new one. |
104
+ | **Building** (a deploy is in flight) | **Error**, naming the version that is already building. Two builds racing for one id is almost always an accident. | **Cancels the in-flight build**, then starts a new one. |
105
+ | **Failed, cancelled or timed out** | **Rebuilds.** No flag needed — builds fail for reasons that have nothing to do with your code. | Rebuilds. |
106
+ | **None** | Builds. | Builds. |
107
+
108
+ Deploying an id that is already live prints the existing version and stops:
109
+
110
+ ```bash
111
+ Version 20250228.1 was already deployed for --external-id abc123 — nothing to build
112
+ ```
113
+
114
+ Deploying an id that is mid-build fails, telling you which version is already building:
115
+
116
+ ```bash
117
+ A deployment for external id "abc123" is already in progress (version 20250228.1). Wait for it to finish, or deploy again with --force to cancel it and start a new one.
118
+ ```
119
+
120
+ The short-circuit on an already-deployed id makes `--external-id` useful on its own as **deploy idempotency**: a CI job that runs twice for the same commit builds once — provided the second run starts after the first has finished — and the short-circuited run still gets the version number in its output.
121
+
122
+ A second run that starts while the first is still building hits the **Building** row above and errors. Only in the brief window before the first run has registered its build do both see "nothing deployed yet"; then both build, and the higher version wins.
123
+
124
+ <Tip>
125
+ In GitHub Actions, the short-circuited run sets the same step outputs (such as
126
+ `deploymentVersion`) as a real build, so downstream steps work unchanged. One exception:
127
+ `needsPromotion` is always `false` on a short-circuit, because a reused version is never promoted —
128
+ so a workflow that gates a promote step on that output will skip promotion on the repeat run.
129
+ </Tip>
130
+
131
+ `--force` starts a new build for an id that already has one. It is non-destructive with respect to deployments that already **succeeded** — both remain, and the newer version wins. It *is* destructive to a build still in flight, which it cancels, because one id should not have two live builds racing to define it. It requires `--external-id`: on its own there is no previous deployment for it to build over, so it errors.
132
+
133
+ <Note>
134
+ A cancelled deployment can never be deployed, so the build it replaces can't land. The build
135
+ itself is signalled to stop and usually does within seconds — but one running elsewhere (a
136
+ `--local-build`, or a deploy from someone else's machine) can keep going for a few minutes before
137
+ it notices.
138
+ </Note>
139
+
140
+ ### Redeploying the same commit
141
+
142
+ The short-circuit is keyed on the id alone, not on what went into the build. If you use the commit SHA as your id and then change something the commit doesn't capture — a synced environment variable, a secret, a dependency resolved at build time — redeploying that commit produces **no new build**, and the deployed image keeps the older inputs.
143
+
144
+ Three ways out, in order of preference:
145
+
146
+ - **Make a new commit.** `git commit --allow-empty -m "redeploy"` gives you a new SHA, which is a new id, which builds. This is the only remedy that also works for the Vercel and GitHub integrations, which never pass `--force`.
147
+ - **Pass `--force`**, if you drive deploys yourself.
148
+ - **Use an id that captures more than the commit** — `${GITHUB_SHA}-${GITHUB_RUN_ID}`, say — if your builds legitimately depend on inputs outside the tree. You lose idempotency in exchange.
149
+
150
+ See the [deploy command reference](/cli-deploy-commands) for all deploy flags.
151
+
152
+ ## Automatic discovery
153
+
154
+ Setting the id yourself is the explicit path, and it always works. The SDK can also **discover** an id from the variables your hosting platform or CI system already injects, so you don't have to wire anything up.
155
+
156
+ Discovery happens at **runtime**, on each trigger call — never at module load — so a prebuilt bundle can't pin a stale value.
157
+
158
+ The SDK takes the first of these that yields a value:
159
+
160
+ 1. **`externalDeploymentId` passed to the trigger call** — always honoured.
161
+ 2. **`externalDeploymentId` passed to `configure()`** — always honoured.
162
+ 3. **`TRIGGER_EXTERNAL_DEPLOYMENT_ID`** — always honoured.
163
+ 4. **Platform and CI variables** — read **only** when `TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION` is set to `1` or `true`.
164
+
165
+ The first three are explicit acts on your part, so they need no opt-in. Only the fourth — reading a commit SHA your platform injected on its own — requires one, because that value is there whether or not anyone asked for this feature.
166
+
167
+ ```bash
168
+ TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION=1
169
+ ```
20
170
 
21
- ## How it works
171
+ The value is trimmed and matched case-insensitively, so `true`, `TRUE` and `True` are all the same thing. Any other value — including `0`, an empty string, or the variable being absent — leaves automatic discovery off. The variable gates *discovery* only; an id you set explicitly is always honoured either way.
22
172
 
23
- Atomic deploys achieve synchronization by deploying your tasks to Trigger.dev without promoting them to the default version. Instead, you explicitly specify the deployed task version in your application’s environment. Here’s the process at a glance:
173
+ <Warning>
174
+ **Most platforms expose the commit SHA to the build, not to the running process.** Discovery reads
175
+ `process.env` inside your live application, so a variable that only exists during the build is a
176
+ variable the SDK cannot see. On those platforms nothing is discovered and **nothing breaks** — your
177
+ runs simply execute on the current version, silently, as they do today. If your platform is marked
178
+ "Build only" below, use the [manual recipe](#the-manual-recipe) instead.
179
+ </Warning>
24
180
 
25
- 1. **Deploy Tasks to Trigger.dev**: Use the Trigger.dev CLI to deploy your tasks with the `--skip-promotion` flag. This creates a new task version without making it the default.
26
- 2. **Capture the Deployment Version**: The CLI outputs the version of the deployed tasks, which you’ll use in the next step.
27
- 3. **Deploy Your Application**: Deploy your application (e.g., to Vercel), setting an environment variable like `TRIGGER_VERSION` to the captured task version.
181
+ ### Hosting platforms
182
+
183
+ Read first, because a hosting variable describes the deployment that is *running*.
184
+
185
+ | Platform | Variable | Available in the running app? |
186
+ | ----------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
187
+ | **Vercel** | `VERCEL_GIT_COMMIT_SHA` | **Yes** — build and runtime, provided "Automatically expose System Environment Variables" is on for the project. |
188
+ | **Railway** | `RAILWAY_GIT_COMMIT_SHA` | **Yes** — injected at build and runtime, no configuration. |
189
+ | **Render** | `RENDER_GIT_COMMIT` | **Yes** — exposed in both build and runtime environments. |
190
+ | **Cloudflare Pages** | `CF_PAGES_COMMIT_SHA` | **Yes** for Pages Functions; also present during the build. |
191
+ | **Cloudflare Workers Builds** | `WORKERS_CI_COMMIT_SHA` | **Build only** — not injected into the deployed Worker. Must be forwarded. |
192
+ | **Netlify** | `COMMIT_REF` | **Build only** in the general case. Must be forwarded. (`CACHED_COMMIT_REF` is the *previous* build's SHA and is not read.) |
193
+ | **AWS Amplify Hosting** | `AWS_COMMIT_ID` | **Build only.** Must be forwarded. |
194
+ | **Heroku** | `HEROKU_BUILD_COMMIT`, then `HEROKU_SLUG_COMMIT` | **Runtime, opt-in** — requires dyno metadata. |
195
+ | **Koyeb** | `KOYEB_GIT_SHA` | **Runtime only** (not available during the build). |
196
+ | **DigitalOcean App Platform** | — | No fixed variable. Bind `${_self.COMMIT_HASH}` to a variable named `COMMIT_HASH` and the generic tier below picks it up. |
197
+ | **Fly.io** | — | Nothing is injected. Set one of the generic names below at deploy time. |
198
+
199
+ ### CI systems
200
+
201
+ Read second. These are correct when your application *is* the CI job, and they are the natural source for `--external-id` in a pipeline. Once the job has ended they no longer exist, so they cannot be discovered from a long-running app.
202
+
203
+ | System | Variable |
204
+ | ------------------- | -------------------- |
205
+ | GitHub Actions | `GITHUB_SHA` |
206
+ | GitLab CI | `CI_COMMIT_SHA` |
207
+ | CircleCI | `CIRCLE_SHA1` |
208
+ | Bitbucket Pipelines | `BITBUCKET_COMMIT` |
209
+ | Buildkite | `BUILDKITE_COMMIT` |
210
+ | Azure Pipelines | `BUILD_SOURCEVERSION` |
211
+ | Google Cloud Build | `COMMIT_SHA` |
212
+ | Drone / Woodpecker | `DRONE_COMMIT_SHA` |
213
+ | Jenkins (git plugin) | `GIT_COMMIT` |
214
+ | TeamCity | `BUILD_VCS_NUMBER` |
215
+ | Travis CI | `TRAVIS_COMMIT` |
216
+
217
+ ### Generic fallbacks
218
+
219
+ Read last, after every named source above is exhausted:
220
+
221
+ ```text
222
+ COMMIT_SHA → COMMIT_HASH → GIT_COMMIT → GIT_SHA → GIT_HASH
223
+ ```
28
224
 
29
- ## Vercel CLI & GitHub Actions
225
+ These are the answer for any platform not listed: set one of them and you get pinning with no other changes. They go last precisely because they are unnamespaced and could plausibly be set by something other than your deploy.
30
226
 
31
- If you deploy to Vercel via their CLI, you can use this sample workflow that demonstrates performing atomic deploys with GitHub Actions, Trigger.dev, and Vercel:
227
+ Empty and whitespace-only values are skipped rather than treated as a match, and a value longer than 128 characters is skipped rather than sent — so a generic name holding something that isn't a deployment id degrades to "no id discovered" instead of an error.
32
228
 
33
- ```yml
34
- name: Deploy to Trigger.dev (prod)
35
- on:
36
- push:
37
- branches:
38
- - main
39
- concurrency:
40
- group: ${{ github.workflow }}
41
- cancel-in-progress: true
42
- jobs:
43
- deploy:
44
- runs-on: ubuntu-latest
45
- steps:
46
- - uses: actions/checkout@v4
229
+ <Tip>
230
+ If a generic name means something else in your environment, you have two clean escapes: set
231
+ `TRIGGER_EXTERNAL_DEPLOYMENT_ID` explicitly (it outranks all discovery), or leave
232
+ `TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION` off and use the manual recipe.
233
+ </Tip>
47
234
 
48
- - name: Use Node.js 20.x
49
- uses: actions/setup-node@v4
50
- with:
51
- node-version: "20.x"
235
+ ## The manual recipe
52
236
 
53
- - name: Install dependencies
54
- run: npm install
237
+ If your platform only exposes the commit at build time — or injects nothing at all — set the id yourself. This is not a lesser mode: it is the same mechanism, and the same code path the Vercel integration uses with both halves filled in automatically.
55
238
 
56
- - name: Deploy Trigger.dev
57
- id: deploy-trigger
58
- env:
59
- TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
60
- run: |
61
- npx trigger.dev@latest deploy --skip-promotion
239
+ Read the commit in the place where it *is* available, and write it into both halves:
62
240
 
63
- - name: Deploy to Vercel
64
- run: npx vercel --yes --prod -e TRIGGER_VERSION=$TRIGGER_VERSION --token $VERCEL_TOKEN
65
- env:
66
- VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
67
- TRIGGER_VERSION: ${{ steps.deploy-trigger.outputs.deploymentVersion }}
241
+ ```bash
242
+ # 1. Deploy side — name the deployment after the commit.
243
+ npx trigger.dev@latest deploy --external-id "$(git rev-parse HEAD)"
68
244
 
69
- - name: Promote Trigger.dev Version
70
- run: npx trigger.dev@latest promote $TRIGGER_VERSION
71
- env:
72
- TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
73
- TRIGGER_VERSION: ${{ steps.deploy-trigger.outputs.deploymentVersion }}
245
+ # 2. Application side — give the running app the same value.
246
+ TRIGGER_EXTERNAL_DEPLOYMENT_ID=<the-same-commit-sha>
74
247
  ```
75
248
 
76
- - Deploy to Trigger.dev
249
+ Substitute a real value on both sides — and prefer something that fails loudly over a bare shell variable. On the platforms this section is about, the platform's own commit variable is usually **not** set in the shell you are deploying from, so `--external-id "$COMMIT_SHA"` would quietly deploy with no id and leave you unprotected.
250
+
251
+ Whatever your platform calls its build-time commit variable — `COMMIT_REF` on Netlify, `AWS_COMMIT_ID` on Amplify, `WORKERS_CI_COMMIT_SHA` on Cloudflare Workers Builds, `$(git rev-parse HEAD)` in a bespoke pipeline — read it there and forward it.
252
+
253
+ Half two can be a runtime environment variable set in your platform's configuration, or a build-time constant inlined into your bundle. Either works.
254
+
255
+ Three things are worth knowing:
77
256
 
78
- - The `npx trigger.dev deploy` command uses `--skip-promotion` to deploy the tasks without setting the version as the default.
79
- - The step’s id: `deploy-trigger` allows us to capture the deployment version in the output (deploymentVersion).
257
+ - **It needs no `TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION`.** That variable gates discovery, not pinning, and an explicitly set id is always honoured.
258
+ - **It is not Vercel-specific, or even git-specific.** A release tag or CI run id is fine — the value only has to match on both sides.
259
+ - **Any name the SDK reads works.** `TRIGGER_EXTERNAL_DEPLOYMENT_ID` is the clearest, but one of the [generic fallbacks](#generic-fallbacks) does the same job.
80
260
 
81
- - Deploy to Vercel:
82
- - The `npx vercel` command deploys the application, setting the `TRIGGER_VERSION` environment variable to the task version from the previous step.
83
- - The --prod flag ensures a production deployment, and -e passes the environment variable.
84
- - The `@trigger.dev/sdk` automatically uses the `TRIGGER_VERSION` environment variable to trigger the correct version of the tasks.
261
+ ## Setting the id in code
85
262
 
86
- For this workflow to work, you need to set up the following secrets in your GitHub repository:
263
+ Environment variables are the usual way, but you can also pass the id directly. Both forms always win over discovery.
87
264
 
88
- - `TRIGGER_ACCESS_TOKEN`: Your Trigger.dev personal access token. View the instructions [here](/github-actions) to learn more.
89
- - `VERCEL_TOKEN`: Your Vercel personal access token. You can find this in your Vercel account settings.
265
+ Per trigger call:
90
266
 
91
- ## Vercel GitHub integration
267
+ ```ts
268
+ import { myTask } from "./trigger/tasks";
92
269
 
93
- If you're are using Vercel, chances are you are using their GitHub integration and deploying your application directly from pushes to GitHub. This section covers how to achieve atomic deploys with Trigger.dev in this setup.
270
+ await myTask.trigger(
271
+ { foo: "bar" },
272
+ { externalDeploymentId: process.env.VERCEL_GIT_COMMIT_SHA }
273
+ );
274
+ ```
275
+
276
+ Or once, for every trigger made by the process:
94
277
 
95
- ### Turn off automatic promotion
278
+ ```ts
279
+ import { configure } from "@trigger.dev/sdk";
280
+
281
+ configure({
282
+ externalDeploymentId: process.env.MY_RELEASE_ID,
283
+ });
284
+ ```
96
285
 
97
- By default, Vercel automatically promotes new deployments to production. To prevent this, you need to disable the auto-promotion feature in your Vercel project settings:
286
+ An empty or whitespace-only value counts as "not supplied" rather than an error, so wiring up a variable that isn't always present is safe.
98
287
 
99
- 1. Go to your Production environment settings in Vercel at `https://vercel.com/<team-slug>/<project-slug>/settings/environments/production`
100
- 2. Disable the "Auto-assign Custom Production Domains" setting:
288
+ 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.
101
289
 
102
- ![Vercel project settings showing the auto-promotion setting](/deployment/auto-assign-production-domains.png)
290
+ ## Chat sessions
103
291
 
104
- 3. Hit the "Save" button to apply the changes.
292
+ [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.
105
293
 
106
- Now whenever you push to your main branch, Vercel will deploy your application to the production environment without promoting it, and you can control the promotion manually.
294
+ 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.
107
295
 
108
- ### Deploy with Trigger.dev
296
+ 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.
109
297
 
110
- Now we want to deploy that same commit to Trigger.dev, and then promote the Vercel deployment when that completes. Here's a sample GitHub Actions workflow that does this:
298
+ ```ts
299
+ // app/actions.ts
300
+ "use server";
301
+ import { chat } from "@trigger.dev/sdk/ai";
302
+ import type { myChat } from "@/trigger/chat";
111
303
 
112
- ```yml
113
- name: Deploy to Trigger.dev (prod)
304
+ // No chat-specific setup: the id is discovered per call, exactly as it is for `trigger()`.
305
+ export const startChatSession = chat.createStartSessionAction<typeof myChat>("my-chat");
306
+ ```
307
+
308
+ Three things follow from the pin living on the session:
309
+
310
+ - **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.
311
+ - **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.
312
+ - **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:
313
+
314
+ ```tsx
315
+ const transport = useTriggerChatTransport({
316
+ task: "my-chat",
317
+ accessToken: ({ chatId }) => mintChatAccessToken(chatId),
318
+ startSession: ({ chatId, clientData }) => startChatSession({ chatId, clientData }),
319
+ onEvent: (event) => {
320
+ if (event.type === "run-pending-version") setDeploying(true);
321
+ if (event.type === "first-chunk") setDeploying(false);
322
+ },
323
+ });
324
+ ```
114
325
 
115
- on:
116
- push:
117
- branches:
118
- - main
326
+ 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.
119
327
 
120
- concurrency:
121
- group: ${{ github.workflow }}
122
- cancel-in-progress: true
328
+ [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.
123
329
 
124
- jobs:
125
- deploy:
126
- runs-on: ubuntu-latest
330
+ ### Opting a chat out
127
331
 
128
- steps:
129
- - uses: actions/checkout@v4
332
+ Pass `null` and that chat is never pinned, whatever the environment says:
130
333
 
131
- - name: Use Node.js 20.x
132
- uses: actions/setup-node@v4
133
- with:
134
- node-version: "20.x"
334
+ ```ts
335
+ export const startChatSession = chat.createStartSessionAction<typeof myChat>("my-chat", {
336
+ triggerConfig: { externalDeploymentId: null },
337
+ });
338
+ ```
135
339
 
136
- - name: Install dependencies
137
- run: npm install
340
+ 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`.
138
341
 
139
- - name: Wait for vercel deployment (push)
140
- id: wait-for-vercel
141
- uses: ludalex/vercel-wait@v1
142
- with:
143
- project-id: ${{ secrets.VERCEL_PROJECT_ID }}
144
- team-id: ${{ secrets.VERCEL_SCOPE_NAME }}
145
- token: ${{ secrets.VERCEL_TOKEN }}
146
- sha: ${{ github.sha }}
342
+ ### Escaping the pin from inside the agent
147
343
 
148
- - name: 🚀 Deploy Trigger.dev
149
- id: deploy-trigger
150
- env:
151
- TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
152
- run: |
153
- npx trigger.dev@latest deploy
344
+ [`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:
154
345
 
155
- - name: Promote Vercel deploy
156
- run: npx vercel promote $VERCEL_DEPLOYMENT_ID --yes --token $VERCEL_TOKEN --scope $VERCEL_SCOPE_NAME
157
- env:
158
- VERCEL_DEPLOYMENT_ID: ${{ steps.wait-for-vercel.outputs.deployment-id }}
159
- VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
160
- VERCEL_SCOPE_NAME: ${{ secrets.VERCEL_SCOPE_NAME }}
346
+ ```ts
347
+ chat.requestUpgrade({ externalDeploymentId: clientData.commitSha });
161
348
  ```
162
349
 
163
- This workflow does the following:
350
+ 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.
351
+
352
+ ## Waiting and expiry
164
353
 
165
- 1. Waits for the Vercel deployment to complete using the `ludalex/vercel-wait` action.
166
- 2. Deploys the tasks to Trigger.dev using the `npx trigger.dev deploy` command. There's no need to use the `--skip-promotion` flag because we want to promote the deployment.
167
- 3. Promotes the Vercel deployment using the `npx vercel promote` command.
354
+ 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.
168
355
 
169
- For this workflow to work, you need to set up the following secrets in your GitHub repository:
356
+ A waiting run shows in the dashboard with the status **Pending version** and the reason _"Waiting for a deployment with this run's external deployment id"_. As soon as a deployment carrying that id is deployed, the run is released and executes pinned to it.
170
357
 
171
- - `TRIGGER_ACCESS_TOKEN`: Your Trigger.dev personal access token. View the instructions [here](/github-actions) to learn more.
172
- - `VERCEL_TOKEN`: Your Vercel personal access token. You can find this in your Vercel account settings.
173
- - `VERCEL_PROJECT_ID`: Your Vercel project ID. You can find this in your Vercel project settings.
174
- - `VERCEL_SCOPE_NAME`: Your Vercel team slug.
358
+ An unknown id waits too, for the same reason: "we haven't seen this deployment" and "this deployment hasn't been created yet" look identical from the outside, and the second is common — your app often goes live before your deploy pipeline has even claimed a runner.
175
359
 
176
- Checkout our [example repo](https://github.com/ericallam/vercel-atomic-deploys) to see this workflow in action.
360
+ <Warning>
361
+ A run waiting on an external deployment id that never arrives **expires after 1 hour**. It moves
362
+ to `EXPIRED` with a message naming the id it waited for — `Run expired because no deployment with
363
+ external id 'abc123' became available`. This bounds — but does not diagnose — the configuration
364
+ mistakes that produce a deployment which never lands. See [When nothing ever
365
+ lands](#when-nothing-ever-lands).
366
+ </Warning>
367
+
368
+ A shorter [run-level `ttl`](/triggering#ttl) that you set yourself still wins — the one-hour deadline is a backstop, not an extension. Note that the run still expires *as* an external-deployment expiry: the status reason and message name the id it was waiting for, not the `ttl`.
177
369
 
178
370
  <Note>
179
- We are using the `ludalex/vercel-wait` action above as a fork of the [official
180
- tj-actions/vercel-wait](https://github.com/tj-actions/vercel-wait) action because there is a bug
181
- in the official action that exits early if the deployment isn't found in the first check and due
182
- to the fact that it supports treating skipped (cancelled) Vercel deployments as valid (on by default).
183
- I've opened a PR for this issue [here](https://github.com/tj-actions/vercel-wait/pull/106).
371
+ This deadline applies only to runs waiting on an external deployment id. Runs that wait for the
372
+ reasons that already exist — such as triggering a task before your first deploy — keep waiting
373
+ indefinitely, unchanged.
184
374
  </Note>
375
+
376
+ ## When nothing ever lands
377
+
378
+ Waiting is the right answer to a race and the wrong answer to a misconfiguration, and for the first hour the two look identical. Three setups produce an id that will never be deployed, and each expires **every** run from the affected release.
379
+
380
+ ### The commit didn't build any tasks
381
+
382
+ The most damaging one, and the easiest to create by accident. If your deploy workflow is filtered on paths — `paths: ['trigger/**']` in a monorepo is the usual shape — a commit that only touches your application produces **no Trigger.dev deployment at all**. Your app still sends that commit's SHA, nothing ever holds it, and every run from that release waits an hour and expires.
383
+
384
+ If you filter deploys by path, derive the id from the same filter on both sides — the SHA of the last commit that touched your task directory, not the commit being deployed:
385
+
386
+ ```bash
387
+ TASK_ID=$(git log -1 --format=%H -- trigger/)
388
+ npx trigger.dev@latest deploy --external-id "$TASK_ID"
389
+ ```
390
+
391
+ Compute the same value where you set your app's variable. In GitHub Actions this needs `fetch-depth: 0` on `actions/checkout` — the default shallow clone can't see far enough back.
392
+
393
+ ### Two applications, one Trigger.dev project
394
+
395
+ If two deployables share one Trigger.dev project and only one of them deploys tasks, the other still sends its own commit SHA — an id nothing will ever hold. Give the application that doesn't build tasks either no id, or the id of the deployment it should run against.
396
+
397
+ ### A preview deployment promoted to production
398
+
399
+ Ids resolve **within a single environment**. An id deployed to a preview branch does not exist in production, so promoting that preview build in your hosting platform — without a production deploy carrying the same id — parks and expires every production run. Deploy to production the normal way rather than promoting a preview artifact.
400
+
401
+ <Tip>
402
+ A run that hit one of these shows as `EXPIRED` with `Run expired because no deployment with
403
+ external id '…' became available`. An environment producing nothing but that message has a
404
+ configuration problem, not a timing problem.
405
+ </Tip>
406
+
407
+ ## Precedence
408
+
409
+ Several things can pin a run. The SDK sends an **explicit version** — the `version` option if you passed one, otherwise `TRIGGER_VERSION` — and the external deployment id, whenever it has them. The server then prefers the explicit version, then the id, then the current version:
410
+
411
+ ```text
412
+ explicit version (version option, else TRIGGER_VERSION) > external deployment id > current version
413
+ ```
414
+
415
+ - An explicit `version` passed to `trigger()` always wins. Because the SDK folds the option and the variable into a single value before sending, the `version` option overrides `TRIGGER_VERSION` on the client, and the server only ever sees the winner.
416
+ - An explicit version therefore beats an external deployment id. This is what makes migration from [legacy atomic deployments](#legacy-atomic-deployments) safe: the legacy pin keeps governing until you remove the variable.
417
+ - An external deployment id sent alongside an explicit version is **ignored, not an error**.
418
+ - A trigger carrying neither runs on the current version.
419
+
420
+ <Warning>
421
+ **Don't leave a stale `TRIGGER_VERSION` behind.** A `TRIGGER_VERSION` naming a version that
422
+ doesn't exist *in the environment being triggered* is not a soft failure and not a fallback —
423
+ **every trigger fails** with a `422`, wherever the trigger came from. It also suppresses the
424
+ external deployment id, so skew protection can't rescue it.
425
+
426
+ The usual way to hit this is setting `TRIGGER_VERSION` at the project level on your hosting
427
+ platform: a version built in production doesn't exist in preview, so every preview trigger fails.
428
+ Remove the variable once you've confirmed skew protection is working.
429
+ </Warning>
430
+
431
+ ```ts
432
+ // Explicit version wins, even if an external deployment id is discovered.
433
+ await myTask.trigger({ foo: "bar" }, { version: "20250228.1" });
434
+ ```
435
+
436
+ ## Automatic skew protection on Vercel
437
+
438
+ If you use the [Vercel integration](/vercel-integration), version skew protection is set up for you and both halves are automatic:
439
+
440
+ - The integration sets **`TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION=1`** on your Vercel project when you connect it, and adds it on the next build if it is missing — so projects connected before this existed pick it up without reconnecting. It only ever adds the variable, never overwrites one that is already there.
441
+ - The integration passes **your commit SHA** as the deploy's external id.
442
+ - `VERCEL_GIT_COMMIT_SHA` is available at runtime on Vercel, so the SDK discovers the matching id with no work from you.
443
+
444
+ This covers production, staging and preview alike.
445
+
446
+ ### Opting out
447
+
448
+ Set `TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION` to `0` on your Vercel project. A `0` you set today stays `0` through every subsequent build.
449
+
450
+ Opting out only disables automatic *discovery*. You can still pin explicitly at any time by setting `TRIGGER_EXTERNAL_DEPLOYMENT_ID` yourself.
451
+
452
+ ## Legacy atomic deployments
453
+
454
+ Before version skew protection, the [Vercel integration](/vercel-integration#atomic-deployments) kept your app and tasks in sync by gating your Vercel deployment until the Trigger.dev build finished, then creating a **second** Vercel deployment with `TRIGGER_VERSION` set to the new task version and promoting it. This needed two Vercel deployments per release, required `Auto-assign Custom Production Domains` to be disabled, and covered production only. The same pattern could be built by hand with `trigger deploy --skip-promotion` and `TRIGGER_VERSION`.
455
+
456
+ <Warning>
457
+ Legacy atomic deployments are **deprecated and not advised** for new setups. The setting still
458
+ works for projects that already have it enabled, and is off by default for new connections. Use
459
+ version skew protection instead.
460
+ </Warning>
461
+
462
+ To migrate:
463
+
464
+ 1. Upgrade your app and deploys to **v4.5.12 or later**.
465
+ 2. Verify on a preview deployment. Atomic deployments only set `TRIGGER_VERSION` on production, so a preview run is pinned by its external deployment id alone. Push a branch, trigger a task from the preview deployment, and check that the run's **External deployment ID** matches the commit SHA and that it ran on that branch's deployment.
466
+ 3. Turn **Atomic deployments** off in your project's Vercel integration settings, and remove `TRIGGER_VERSION` from your Vercel production environment. Until you remove it, it outranks the external deployment id.
467
+ 4. Put production promotion back in place: re-enable `Auto-assign Custom Production Domains` in Vercel, or promote from your own pipeline. Atomic deployments turned auto-assign off, so without this a new production deployment is never served on your domain.
468
+ 5. Deploy to production and run the same check there.
@@ -121,7 +121,7 @@ TRIGGER_VERSION=20250228.1
121
121
  <Tip>
122
122
  If what you actually want is for each release of your app to run against the tasks built from the
123
123
  same commit, you don't need to plumb version numbers around by hand. See [version skew
124
- protection](/deployment/version-skew-protection).
124
+ protection](/deployment/atomic-deployment).
125
125
  </Tip>
126
126
 
127
127
  ### Child tasks and auto-version locking
@@ -157,7 +157,7 @@ Or from the dashboard:
157
157
 
158
158
  ![Trigger.dev dashboard showing the promote button](/deployment/promote-button.png)
159
159
 
160
- To learn more about skipping promotion and how this enables atomic deployments, see our [Atomic deployment](/deployment/atomic-deployment) guide. To keep your app and tasks in sync without coordinating promotion at all, see [version skew protection](/deployment/version-skew-protection).
160
+ To keep your app and tasks in sync without coordinating promotion at all, see [atomic deployments](/deployment/atomic-deployment).
161
161
 
162
162
  ## Staging deploys
163
163