@trigger.dev/sdk 4.5.11 → 4.5.13

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 (102) hide show
  1. package/dist/commonjs/v3/ai.d.ts +154 -20
  2. package/dist/commonjs/v3/ai.js +1247 -406
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/auth.d.ts +3 -3
  5. package/dist/commonjs/v3/chat-client.d.ts +5 -4
  6. package/dist/commonjs/v3/chat-client.js +2 -6
  7. package/dist/commonjs/v3/chat-client.js.map +1 -1
  8. package/dist/commonjs/v3/chat.d.ts +7 -2
  9. package/dist/commonjs/v3/chat.js +22 -7
  10. package/dist/commonjs/v3/chat.js.map +1 -1
  11. package/dist/commonjs/v3/chat.test.js +13 -4
  12. package/dist/commonjs/v3/chat.test.js.map +1 -1
  13. package/dist/commonjs/v3/envvars.js.map +1 -1
  14. package/dist/commonjs/v3/retry.d.ts +1 -1
  15. package/dist/commonjs/v3/retry.js +1 -2
  16. package/dist/commonjs/v3/retry.js.map +1 -1
  17. package/dist/commonjs/v3/runs.d.ts +2 -6
  18. package/dist/commonjs/v3/runs.js.map +1 -1
  19. package/dist/commonjs/v3/schedules/api.d.ts +8 -1
  20. package/dist/commonjs/v3/schedules/index.d.ts +11 -6
  21. package/dist/commonjs/v3/schedules/index.js +2 -0
  22. package/dist/commonjs/v3/schedules/index.js.map +1 -1
  23. package/dist/commonjs/v3/schedules/index.test.d.ts +1 -0
  24. package/dist/commonjs/v3/schedules/index.test.js +49 -0
  25. package/dist/commonjs/v3/schedules/index.test.js.map +1 -0
  26. package/dist/commonjs/v3/schedules/index.types.test.d.ts +1 -0
  27. package/dist/commonjs/v3/schedules/index.types.test.js +93 -0
  28. package/dist/commonjs/v3/schedules/index.types.test.js.map +1 -0
  29. package/dist/commonjs/v3/sessions.d.ts +4 -10
  30. package/dist/commonjs/v3/sessions.js +73 -47
  31. package/dist/commonjs/v3/sessions.js.map +1 -1
  32. package/dist/commonjs/v3/shared.d.ts +2 -3
  33. package/dist/commonjs/v3/shared.js +14 -0
  34. package/dist/commonjs/v3/shared.js.map +1 -1
  35. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +1 -1
  36. package/dist/commonjs/v3/test/mock-chat-agent.js +1 -0
  37. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  38. package/dist/commonjs/v3/test/test-session-handle.d.ts +1 -31
  39. package/dist/commonjs/v3/test/test-session-handle.js +22 -25
  40. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  41. package/dist/commonjs/version.js +1 -1
  42. package/dist/esm/v3/ai.d.ts +154 -20
  43. package/dist/esm/v3/ai.js +1245 -406
  44. package/dist/esm/v3/ai.js.map +1 -1
  45. package/dist/esm/v3/auth.d.ts +3 -3
  46. package/dist/esm/v3/chat-client.d.ts +5 -4
  47. package/dist/esm/v3/chat-client.js +2 -6
  48. package/dist/esm/v3/chat-client.js.map +1 -1
  49. package/dist/esm/v3/chat.d.ts +7 -2
  50. package/dist/esm/v3/chat.js +22 -7
  51. package/dist/esm/v3/chat.js.map +1 -1
  52. package/dist/esm/v3/chat.test.js +13 -4
  53. package/dist/esm/v3/chat.test.js.map +1 -1
  54. package/dist/esm/v3/envvars.js.map +1 -1
  55. package/dist/esm/v3/retry.d.ts +1 -1
  56. package/dist/esm/v3/retry.js +1 -2
  57. package/dist/esm/v3/retry.js.map +1 -1
  58. package/dist/esm/v3/runs.d.ts +2 -6
  59. package/dist/esm/v3/runs.js.map +1 -1
  60. package/dist/esm/v3/schedules/api.d.ts +8 -1
  61. package/dist/esm/v3/schedules/index.d.ts +11 -6
  62. package/dist/esm/v3/schedules/index.js +2 -0
  63. package/dist/esm/v3/schedules/index.js.map +1 -1
  64. package/dist/esm/v3/schedules/index.test.d.ts +1 -0
  65. package/dist/esm/v3/schedules/index.test.js +47 -0
  66. package/dist/esm/v3/schedules/index.test.js.map +1 -0
  67. package/dist/esm/v3/schedules/index.types.test.d.ts +1 -0
  68. package/dist/esm/v3/schedules/index.types.test.js +91 -0
  69. package/dist/esm/v3/schedules/index.types.test.js.map +1 -0
  70. package/dist/esm/v3/sessions.d.ts +4 -10
  71. package/dist/esm/v3/sessions.js +73 -47
  72. package/dist/esm/v3/sessions.js.map +1 -1
  73. package/dist/esm/v3/shared.d.ts +2 -3
  74. package/dist/esm/v3/shared.js +15 -1
  75. package/dist/esm/v3/shared.js.map +1 -1
  76. package/dist/esm/v3/test/mock-chat-agent.d.ts +1 -1
  77. package/dist/esm/v3/test/mock-chat-agent.js +2 -1
  78. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  79. package/dist/esm/v3/test/test-session-handle.d.ts +1 -31
  80. package/dist/esm/v3/test/test-session-handle.js +24 -25
  81. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  82. package/dist/esm/version.js +1 -1
  83. package/docs/ai-chat/client-protocol.mdx +8 -3
  84. package/docs/ai-chat/custom-agents.mdx +181 -46
  85. package/docs/ai-chat/patterns/recovery-boot.mdx +9 -2
  86. package/docs/ai-chat/patterns/version-upgrades.mdx +26 -6
  87. package/docs/ai-chat/pending-messages.mdx +5 -3
  88. package/docs/ai-chat/reference.mdx +26 -10
  89. package/docs/ai-chat/types.mdx +5 -1
  90. package/docs/apikeys.mdx +159 -27
  91. package/docs/deployment/atomic-deployment.mdx +12 -0
  92. package/docs/deployment/overview.mdx +7 -1
  93. package/docs/deployment/version-skew-protection.mdx +430 -0
  94. package/docs/github-actions.mdx +33 -5
  95. package/docs/github-integration.mdx +12 -0
  96. package/docs/realtime/auth.mdx +3 -1
  97. package/docs/self-hosting/env/supervisor.mdx +1 -0
  98. package/docs/self-hosting/env/webapp.mdx +1 -0
  99. package/docs/tasks/scheduled.mdx +45 -0
  100. package/docs/vercel-integration.mdx +43 -9
  101. package/docs/versioning.mdx +2 -0
  102. package/package.json +3 -15
@@ -0,0 +1,430 @@
1
+ ---
2
+ title: "Version skew protection"
3
+ description: "Pin task runs to the deployment that matches the release of your app that triggered them."
4
+ ---
5
+
6
+ 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**.
7
+
8
+ 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.
9
+
10
+ <Note>
11
+ Version skew protection requires `@trigger.dev/sdk` and the `trigger.dev` CLI on [v4.5.12 or
12
+ later](https://github.com/triggerdotdev/trigger.dev/releases/tag/v4.5.12), the release that
13
+ introduced external deployment ids. On an older version no id is sent, and runs execute on the
14
+ current version with no warning.
15
+ </Note>
16
+
17
+ ## The guarantee
18
+
19
+ Once your app is sending an external deployment id, a triggered run has one of four outcomes:
20
+
21
+ | Situation | What happens |
22
+ | ---------------------------------------------------- | ------------------------------------------------------------------- |
23
+ | A deployment with that id is live | The run is **pinned** to it and executes immediately. |
24
+ | A deployment with that id is still building | The run **waits**, then executes pinned to it once the build lands. |
25
+ | No deployment with that id ever arrives | The run waits up to **1 hour**, then expires. |
26
+ | No id was sent at all | Nothing changes — the run executes on the current version. |
27
+
28
+ **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.
29
+
30
+ This works identically in production, staging and preview (including per-branch preview deployments), with no per-environment configuration.
31
+
32
+ ## Quick start
33
+
34
+ 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**.
35
+
36
+ <Steps>
37
+
38
+ <Step title="Deploy your tasks with an external id">
39
+
40
+ ```bash
41
+ npx trigger.dev@latest deploy --external-id "$(git rev-parse HEAD)"
42
+ ```
43
+
44
+ </Step>
45
+
46
+ <Step title="Give your running app the same value">
47
+
48
+ Set this in your hosting platform's runtime environment variables, alongside `TRIGGER_SECRET_KEY`:
49
+
50
+ ```bash
51
+ TRIGGER_EXTERNAL_DEPLOYMENT_ID=<the-same-commit-sha>
52
+ ```
53
+
54
+ </Step>
55
+
56
+ </Steps>
57
+
58
+ <Warning>
59
+ Whatever you use on both sides, make sure it can't expand to an empty string. An unset shell
60
+ variable deploys with no id at all, which silently gives you no protection rather than an error.
61
+ </Warning>
62
+
63
+ That's it. Every task triggered by that release of your app now runs on the deployment you built for it.
64
+
65
+ 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).
66
+
67
+ ## Deploying with an external id
68
+
69
+ ```bash
70
+ npx trigger.dev@latest deploy --external-id <value>
71
+ ```
72
+
73
+ 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**.
74
+
75
+ ### Reusing an id
76
+
77
+ 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:
78
+
79
+ | Existing deployment for this id | Default behaviour | With `--force` |
80
+ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------ |
81
+ | **Deployed** | **No build.** The CLI reports the existing version, sets the same outputs, and exits successfully. | Builds a new one. |
82
+ | **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. |
83
+ | **Failed, cancelled or timed out** | **Rebuilds.** No flag needed — builds fail for reasons that have nothing to do with your code. | Rebuilds. |
84
+ | **None** | Builds. | Builds. |
85
+
86
+ Deploying an id that is already live prints the existing version and stops:
87
+
88
+ ```bash
89
+ Version 20250228.1 was already deployed for --external-id abc123 — nothing to build
90
+ ```
91
+
92
+ Deploying an id that is mid-build fails, telling you which version is already building:
93
+
94
+ ```bash
95
+ 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.
96
+ ```
97
+
98
+ 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.
99
+
100
+ 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.
101
+
102
+ <Tip>
103
+ In GitHub Actions, the short-circuited run sets the same step outputs (such as
104
+ `deploymentVersion`) as a real build, so downstream steps work unchanged. One exception:
105
+ `needsPromotion` is always `false` on a short-circuit, because a reused version is never promoted —
106
+ so a workflow that gates a promote step on that output will skip promotion on the repeat run.
107
+ </Tip>
108
+
109
+ `--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.
110
+
111
+ <Note>
112
+ A cancelled deployment can never be deployed, so the build it replaces can't land. The build
113
+ itself is signalled to stop and usually does within seconds — but one running elsewhere (a
114
+ `--local-build`, or a deploy from someone else's machine) can keep going for a few minutes before
115
+ it notices.
116
+ </Note>
117
+
118
+ ### Redeploying the same commit
119
+
120
+ 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.
121
+
122
+ Three ways out, in order of preference:
123
+
124
+ - **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`.
125
+ - **Pass `--force`**, if you drive deploys yourself.
126
+ - **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.
127
+
128
+ See the [deploy command reference](/cli-deploy-commands) for all deploy flags.
129
+
130
+ ## Automatic discovery
131
+
132
+ 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.
133
+
134
+ Discovery happens at **runtime**, on each trigger call — never at module load — so a prebuilt bundle can't pin a stale value.
135
+
136
+ The SDK takes the first of these that yields a value:
137
+
138
+ 1. **`externalDeploymentId` passed to the trigger call** — always honoured.
139
+ 2. **`externalDeploymentId` passed to `configure()`** — always honoured.
140
+ 3. **`TRIGGER_EXTERNAL_DEPLOYMENT_ID`** — always honoured.
141
+ 4. **Platform and CI variables** — read **only** when `TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION` is set to `1` or `true`.
142
+
143
+ 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.
144
+
145
+ ```bash
146
+ TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION=1
147
+ ```
148
+
149
+ 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.
150
+
151
+ <Warning>
152
+ **Most platforms expose the commit SHA to the build, not to the running process.** Discovery reads
153
+ `process.env` inside your live application, so a variable that only exists during the build is a
154
+ variable the SDK cannot see. On those platforms nothing is discovered and **nothing breaks** — your
155
+ runs simply execute on the current version, silently, as they do today. If your platform is marked
156
+ "Build only" below, use the [manual recipe](#the-manual-recipe) instead.
157
+ </Warning>
158
+
159
+ ### Hosting platforms
160
+
161
+ Read first, because a hosting variable describes the deployment that is *running*.
162
+
163
+ | Platform | Variable | Available in the running app? |
164
+ | ----------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
165
+ | **Vercel** | `VERCEL_GIT_COMMIT_SHA` | **Yes** — build and runtime, provided "Automatically expose System Environment Variables" is on for the project. |
166
+ | **Railway** | `RAILWAY_GIT_COMMIT_SHA` | **Yes** — injected at build and runtime, no configuration. |
167
+ | **Render** | `RENDER_GIT_COMMIT` | **Yes** — exposed in both build and runtime environments. |
168
+ | **Cloudflare Pages** | `CF_PAGES_COMMIT_SHA` | **Yes** for Pages Functions; also present during the build. |
169
+ | **Cloudflare Workers Builds** | `WORKERS_CI_COMMIT_SHA` | **Build only** — not injected into the deployed Worker. Must be forwarded. |
170
+ | **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.) |
171
+ | **AWS Amplify Hosting** | `AWS_COMMIT_ID` | **Build only.** Must be forwarded. |
172
+ | **Heroku** | `HEROKU_BUILD_COMMIT`, then `HEROKU_SLUG_COMMIT` | **Runtime, opt-in** — requires dyno metadata. |
173
+ | **Koyeb** | `KOYEB_GIT_SHA` | **Runtime only** (not available during the build). |
174
+ | **DigitalOcean App Platform** | — | No fixed variable. Bind `${_self.COMMIT_HASH}` to a variable named `COMMIT_HASH` and the generic tier below picks it up. |
175
+ | **Fly.io** | — | Nothing is injected. Set one of the generic names below at deploy time. |
176
+
177
+ ### CI systems
178
+
179
+ 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.
180
+
181
+ | System | Variable |
182
+ | ------------------- | -------------------- |
183
+ | GitHub Actions | `GITHUB_SHA` |
184
+ | GitLab CI | `CI_COMMIT_SHA` |
185
+ | CircleCI | `CIRCLE_SHA1` |
186
+ | Bitbucket Pipelines | `BITBUCKET_COMMIT` |
187
+ | Buildkite | `BUILDKITE_COMMIT` |
188
+ | Azure Pipelines | `BUILD_SOURCEVERSION` |
189
+ | Google Cloud Build | `COMMIT_SHA` |
190
+ | Drone / Woodpecker | `DRONE_COMMIT_SHA` |
191
+ | Jenkins (git plugin) | `GIT_COMMIT` |
192
+ | TeamCity | `BUILD_VCS_NUMBER` |
193
+ | Travis CI | `TRAVIS_COMMIT` |
194
+
195
+ ### Generic fallbacks
196
+
197
+ Read last, after every named source above is exhausted:
198
+
199
+ ```text
200
+ COMMIT_SHA → COMMIT_HASH → GIT_COMMIT → GIT_SHA → GIT_HASH
201
+ ```
202
+
203
+ 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.
204
+
205
+ 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.
206
+
207
+ <Tip>
208
+ If a generic name means something else in your environment, you have two clean escapes: set
209
+ `TRIGGER_EXTERNAL_DEPLOYMENT_ID` explicitly (it outranks all discovery), or leave
210
+ `TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION` off and use the manual recipe.
211
+ </Tip>
212
+
213
+ ## The manual recipe
214
+
215
+ 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.
216
+
217
+ Read the commit in the place where it *is* available, and write it into both halves:
218
+
219
+ ```bash
220
+ # 1. Deploy side — name the deployment after the commit.
221
+ npx trigger.dev@latest deploy --external-id "$(git rev-parse HEAD)"
222
+
223
+ # 2. Application side — give the running app the same value.
224
+ TRIGGER_EXTERNAL_DEPLOYMENT_ID=<the-same-commit-sha>
225
+ ```
226
+
227
+ 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.
228
+
229
+ 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.
230
+
231
+ 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.
232
+
233
+ Three things are worth knowing:
234
+
235
+ - **It needs no `TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION`.** That variable gates discovery, not pinning, and an explicitly set id is always honoured.
236
+ - **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.
237
+ - **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.
238
+
239
+ ### Example: Netlify
240
+
241
+ Netlify exposes `COMMIT_REF` to the build only, so forward it:
242
+
243
+ ```bash
244
+ # In your build command or CI step
245
+ npx trigger.dev@latest deploy --external-id "$COMMIT_REF"
246
+ ```
247
+
248
+ Then set `TRIGGER_EXTERNAL_DEPLOYMENT_ID` to the same commit in your site's runtime environment variables — for example by writing it during the build, or by having your deploy pipeline set it through the Netlify API.
249
+
250
+ ## Setting the id in code
251
+
252
+ Environment variables are the usual way, but you can also pass the id directly. Both forms always win over discovery.
253
+
254
+ Per trigger call:
255
+
256
+ ```ts
257
+ import { myTask } from "./trigger/tasks";
258
+
259
+ await myTask.trigger(
260
+ { foo: "bar" },
261
+ { externalDeploymentId: process.env.VERCEL_GIT_COMMIT_SHA }
262
+ );
263
+ ```
264
+
265
+ Or once, for every trigger made by the process:
266
+
267
+ ```ts
268
+ import { configure } from "@trigger.dev/sdk";
269
+
270
+ configure({
271
+ externalDeploymentId: process.env.MY_RELEASE_ID,
272
+ });
273
+ ```
274
+
275
+ 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.
276
+
277
+ Batch triggers carry the id too. `batchTrigger` resolves it per item exactly as `trigger` does, and it survives the asynchronous materialisation of batch items — so a large batch triggered during a deploy waits and releases item by item, each pinned to the deployment its calling code came from.
278
+
279
+ ## Waiting and expiry
280
+
281
+ 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.
282
+
283
+ 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.
284
+
285
+ 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.
286
+
287
+ <Warning>
288
+ A run waiting on an external deployment id that never arrives **expires after 1 hour**. It moves
289
+ to `EXPIRED` with a message naming the id it waited for — `Run expired because no deployment with
290
+ external id 'abc123' became available`. This bounds — but does not diagnose — the configuration
291
+ mistakes that produce a deployment which never lands. See [When nothing ever
292
+ lands](#when-nothing-ever-lands).
293
+ </Warning>
294
+
295
+ 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`.
296
+
297
+ <Note>
298
+ This deadline applies only to runs waiting on an external deployment id. Runs that wait for the
299
+ reasons that already exist — such as triggering a task before your first deploy — keep waiting
300
+ indefinitely, unchanged.
301
+ </Note>
302
+
303
+ ## When nothing ever lands
304
+
305
+ 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.
306
+
307
+ ### The commit didn't build any tasks
308
+
309
+ 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.
310
+
311
+ 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:
312
+
313
+ ```bash
314
+ TASK_ID=$(git log -1 --format=%H -- trigger/)
315
+ npx trigger.dev@latest deploy --external-id "$TASK_ID"
316
+ ```
317
+
318
+ 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.
319
+
320
+ ### Two applications, one Trigger.dev project
321
+
322
+ 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.
323
+
324
+ ### A preview deployment promoted to production
325
+
326
+ 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.
327
+
328
+ <Tip>
329
+ A run that hit one of these shows as `EXPIRED` with `Run expired because no deployment with
330
+ external id '…' became available`. An environment producing nothing but that message has a
331
+ configuration problem, not a timing problem.
332
+ </Tip>
333
+
334
+ ## Precedence
335
+
336
+ 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:
337
+
338
+ ```text
339
+ explicit version (version option, else TRIGGER_VERSION) > external deployment id > current version
340
+ ```
341
+
342
+ - 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.
343
+ - An explicit version therefore beats an external deployment id. This is what makes migration from [atomic deploys](/deployment/atomic-deployment) safe: the legacy pin keeps governing until you remove the variable.
344
+ - An external deployment id sent alongside an explicit version is **ignored, not an error**.
345
+ - A trigger carrying neither runs on the current version.
346
+
347
+ <Warning>
348
+ **Don't leave a stale `TRIGGER_VERSION` behind.** A `TRIGGER_VERSION` naming a version that
349
+ doesn't exist *in the environment being triggered* is not a soft failure and not a fallback —
350
+ **every trigger fails** with a `422`, wherever the trigger came from. It also suppresses the
351
+ external deployment id, so skew protection can't rescue it.
352
+
353
+ The usual way to hit this is setting `TRIGGER_VERSION` at the project level on your hosting
354
+ platform: a version built in production doesn't exist in preview, so every preview trigger fails.
355
+ Remove the variable once you've confirmed skew protection is working.
356
+ </Warning>
357
+
358
+ ```ts
359
+ // Explicit version wins, even if an external deployment id is discovered.
360
+ await myTask.trigger({ foo: "bar" }, { version: "20250228.1" });
361
+ ```
362
+
363
+ ## Automatic skew protection on Vercel
364
+
365
+ If you use the [Vercel integration](/vercel-integration), version skew protection is set up for you and both halves are automatic:
366
+
367
+ - 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.
368
+ - The integration passes **your commit SHA** as the deploy's external id.
369
+ - `VERCEL_GIT_COMMIT_SHA` is available at runtime on Vercel, so the SDK discovers the matching id with no work from you.
370
+
371
+ This covers production, staging and preview alike.
372
+
373
+ ### Opting out
374
+
375
+ Set `TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION` to `0` on your Vercel project. A `0` you set today stays `0` through every subsequent build.
376
+
377
+ Opting out only disables automatic *discovery*. You can still pin explicitly at any time by setting `TRIGGER_EXTERNAL_DEPLOYMENT_ID` yourself.
378
+
379
+ ## Replacing automatic atomic deployments
380
+
381
+ <Warning>
382
+ **Automatic atomic deployments are deprecated.** The setting still exists in your Vercel
383
+ integration settings, is labelled Deprecated, and keeps working — nothing is being switched off
384
+ for you. New connections have it off by default. Version skew protection is the supported
385
+ mechanism.
386
+ </Warning>
387
+
388
+ [Atomic deployments](/vercel-integration#atomic-deployments) solved the same problem by gating your Vercel deployment on the Trigger.dev build, then spawning a **second** Vercel deployment with `TRIGGER_VERSION` baked in and promoting it.
389
+
390
+ Version skew protection replaces it and is strictly less invasive:
391
+
392
+ | | Automatic atomic deployments | Version skew protection |
393
+ | --- | --- | --- |
394
+ | Vercel deployments per release | Two — the original, then a redeploy | One |
395
+ | Blocks your Vercel deployment | Yes, until the task build finishes | Never |
396
+ | `Auto-assign Custom Production Domains` | Must be disabled | Untouched |
397
+ | Environments covered | Production only | Production, staging **and** preview |
398
+ | Configuration | A setting on the production environment | None — the id is the contract |
399
+ | Runs triggered before the build lands | Run on the previous version | Wait, then run on the right version |
400
+
401
+ ### Migrating
402
+
403
+ 1. Make sure your app and your deploys are on **v4.5.12 or later** — the release that introduced external deployment ids. The CLI (`trigger.dev`) and the SDK (`@trigger.dev/sdk`) version together, so `npx trigger.dev@latest update` moves both; see the [v4.5.12 release notes](https://github.com/triggerdotdev/trigger.dev/releases/tag/v4.5.12).
404
+ 2. Confirm skew protection is working — deploy, then check that runs triggered by the new release are pinned to the new version.
405
+ 3. Turn **Atomic deployments** off in your project's Vercel settings.
406
+ 4. **Remove the `TRIGGER_VERSION` variable** from your Vercel project. Until you do, it outranks the external deployment id and keeps governing.
407
+ 5. Decide who promotes. Under atomic deployments Trigger.dev promoted for you; without it, either re-enable `Auto-assign Custom Production Domains` in Vercel or promote deliberately from your own pipeline.
408
+
409
+ Both mechanisms can run at the same time without conflict while the variable still names a live version — `TRIGGER_VERSION` simply keeps winning — so there is no broken intermediate state and no need to do all of this at once.
410
+
411
+ <Warning>
412
+ **Step 4 is not optional, and don't do it before step 2.** While `TRIGGER_VERSION` names a version
413
+ that exists in the environment being triggered, it simply keeps winning — safe, and why the two
414
+ mechanisms coexist. The moment it names a version that environment doesn't have, **every trigger
415
+ fails outright** with a `422`, and the external deployment id it suppresses can't catch it.
416
+ </Warning>
417
+
418
+ <Note>
419
+ If you want your Vercel deployment to stay **gated** on the Trigger.dev build, atomic deployments
420
+ remain the way to do that. Version skew protection never blocks a deployment; it makes blocking
421
+ unnecessary by making runs wait instead.
422
+ </Note>
423
+
424
+ ## Related
425
+
426
+ - [Deployment overview](/deployment/overview) — versions, current version and version locking
427
+ - [Deploy CLI command](/cli-deploy-commands) — every deploy flag
428
+ - [Vercel integration](/vercel-integration)
429
+ - [Preview branches](/deployment/preview-branches)
430
+ - [Atomic deploys](/deployment/atomic-deployment) (deprecated for Vercel)
@@ -11,11 +11,11 @@ The instructions below are specific to GitHub Actions, but the same concepts can
11
11
 
12
12
  ## GitHub Actions example
13
13
 
14
- This simple GitHub action workflow will deploy your Trigger.dev tasks when new code is pushed to the `main` branch and the `trigger` directory has changes in it.
14
+ This simple GitHub action workflow will deploy your Trigger.dev tasks when new code is pushed to the `main` branch.
15
15
 
16
16
  <Warning>
17
17
  The deploy step will fail if any version mismatches are detected. Please see the [version
18
- pinning](/github-actions#version-pinning) section for more details.
18
+ pinning](/github-actions#cli-version-pinning) section for more details.
19
19
  </Warning>
20
20
 
21
21
  <CodeGroup>
@@ -47,7 +47,7 @@ jobs:
47
47
  env:
48
48
  TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
49
49
  run: |
50
- npx trigger.dev@latest deploy
50
+ npx trigger.dev@latest deploy --external-id ${{ github.sha }}
51
51
  ```
52
52
 
53
53
  ```yaml .github/workflows/release-trigger-staging.yml
@@ -76,13 +76,37 @@ jobs:
76
76
  env:
77
77
  TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
78
78
  run: |
79
- npx trigger.dev@latest deploy --env staging
79
+ npx trigger.dev@latest deploy --env staging --external-id ${{ github.sha }}
80
80
  ```
81
81
 
82
82
  </CodeGroup>
83
83
 
84
84
  If you already have a GitHub action file, you can just add the final step "🚀 Deploy Trigger.dev" to your existing file.
85
85
 
86
+ ### Pinning runs to the deployment you just built
87
+
88
+ The `--external-id ${{ github.sha }}` above tags the deployment with the commit it was built from. That is the first half of [version skew protection](/deployment/version-skew-protection): to complete it, give your running application the **same value** so it sends that id when it triggers.
89
+
90
+ ```bash
91
+ # In your application's runtime environment, for the release built from this commit
92
+ TRIGGER_EXTERNAL_DEPLOYMENT_ID=<the-same-commit-sha>
93
+ ```
94
+
95
+ Every task triggered by that release is then pinned to the deployment built from the same commit — and runs triggered before the task build finishes wait for it rather than executing on the previous version.
96
+
97
+ <Note>
98
+ Tagging the deployment is harmless on its own: if nothing sends a matching id, runs behave exactly
99
+ as they do today. `--external-id` also makes repeat deploys idempotent — re-running the workflow
100
+ for a commit that is already deployed reports the existing version instead of building again.
101
+
102
+ Two things follow from that. Because no build runs, a re-run after changing a synced environment
103
+ variable won't pick the new value up — make an empty commit, or pass `--force`. And if you add a
104
+ `paths:` filter to this workflow, `${{ github.sha }}` stops being a safe id: commits that don't
105
+ touch your tasks never produce a deployment carrying that SHA, so every run from those releases
106
+ expires. See [when nothing ever
107
+ lands](/deployment/version-skew-protection#when-nothing-ever-lands).
108
+ </Note>
109
+
86
110
  ## Preview branches
87
111
 
88
112
  To deploy to preview branches from Pull Requests and have them archived when PRs are merged or closed, use a workflow that runs on `pull_request` with **all four types** including `closed`:
@@ -109,11 +133,15 @@ jobs:
109
133
  run: npm install
110
134
 
111
135
  - name: Deploy preview branch
112
- run: npx trigger.dev@latest deploy --env preview
136
+ run: npx trigger.dev@latest deploy --env preview --external-id ${{ github.event.pull_request.head.sha }}
113
137
  env:
114
138
  TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
115
139
  ```
116
140
 
141
+ On `pull_request`, `github.sha` is the merge commit GitHub creates for the PR, not the commit your
142
+ app was built from. Use `github.event.pull_request.head.sha` so the id matches the one your preview
143
+ deployment sends.
144
+
117
145
  <Note>
118
146
  **Include `closed`** in the `pull_request.types` list. Without it, preview branches won't be archived when PRs are merged or closed, and you may hit the limit on active preview branches. See [Preview branches](/deployment/preview-branches#preview-branches-with-github-actions-recommended) for more details.
119
147
  </Note>
@@ -72,6 +72,18 @@ The name of the preview branch matches the branch name of the pull request.
72
72
  Preview branch deployments require the preview environment to be enabled on your project. Learn more about [preview branches](/deployment/preview-branches).
73
73
  </Note>
74
74
 
75
+ ## Version skew protection
76
+
77
+ Every deployment the GitHub integration creates is tagged with the commit SHA it was built from. That is the deploy half of [version skew protection](/deployment/version-skew-protection) — you get it for free.
78
+
79
+ To complete it, give your running application the same value. Unlike the Vercel integration, we have no access to wherever your app is hosted, so this half is yours to set:
80
+
81
+ ```bash
82
+ TRIGGER_EXTERNAL_DEPLOYMENT_ID=<the-commit-sha-this-release-was-built-from>
83
+ ```
84
+
85
+ If your host already exposes the commit SHA at runtime, set `TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION=1` instead and the SDK will find it — see the [platform table](/deployment/version-skew-protection#hosting-platforms).
86
+
75
87
  ## Disconnecting a repository
76
88
 
77
89
  You can disconnect a repository at any time from your project git settings. This will stop automatic deployments triggered from GitHub.
@@ -128,9 +128,11 @@ const publicToken = await auth.createPublicToken({
128
128
  ```
129
129
 
130
130
  - If `expirationTime` is a string, it will be treated as a time span
131
- - If `expirationTime` is a number, it will be treated as a Unix timestamp
131
+ - If `expirationTime` is a number, it will be treated as a Unix timestamp in **seconds**
132
132
  - If `expirationTime` is a `Date`, it will be treated as a date
133
133
 
134
+ When using non-root API keys (recommended), the expiration cannot be more than 30 days in the future.
135
+
134
136
  The format used for a time span is the same as the [jose package](https://github.com/panva/jose), which is a number followed by a unit. Valid units are: "sec", "secs", "second", "seconds", "s", "minute", "minutes", "min", "mins", "m", "hour", "hours", "hr", "hrs", "h", "day", "days", "d", "week", "weeks", "w", "year", "years", "yr", "yrs", and "y". It is not possible to specify months. 365.25 days is used as an alias for a year. If the string is suffixed with "ago", or prefixed with a "-", the resulting time span gets subtracted from the current unix timestamp. A "from now" suffix can also be used for readability when adding to the current unix timestamp.
135
137
 
136
138
  ### Auto-generated tokens
@@ -48,6 +48,7 @@ mode: "wide"
48
48
  | `KUBERNETES_NAMESPACE` | No | default | The namespace that runs should be in. |
49
49
  | `KUBERNETES_WORKER_NODETYPE_LABEL` | No | v4-worker | Nodes for runs need `nodetype=<this>`. Empty: any node. |
50
50
  | `KUBERNETES_RUNNER_TOLERATIONS` | No | — | Run pod tolerations. CSV: `key=value:effect`/`key:effect`. |
51
+ | `KUBERNETES_ORG_PLACEMENT_OVERRIDES` | No | — | Per-org run pod placement. JSON keyed by internal org ID. |
51
52
  | `KUBERNETES_IMAGE_PULL_SECRETS` | No | — | Image pull secrets (CSV). |
52
53
  | `KUBERNETES_EPHEMERAL_STORAGE_SIZE_LIMIT` | No | 10Gi | Ephemeral storage size limit. Applies to all runs. |
53
54
  | `KUBERNETES_EPHEMERAL_STORAGE_SIZE_REQUEST` | No | 2Gi | Ephemeral storage size request. Applies to all runs. |
@@ -184,6 +184,7 @@ mode: "wide"
184
184
  | `MACHINE_PRESETS_OVERRIDE_PATH` | No | — | Path to machine presets override file. See [machine overrides](/self-hosting/overview#machine-overrides). |
185
185
  | `APP_ENV` | No | `NODE_ENV` | App environment. Used for things like the title tag. |
186
186
  | `ADMIN_EMAILS` | No | — | Regex of user emails to automatically promote to admin on signup. Does not apply to existing users. |
187
+ | `ADMIN_DASHBOARD_ENABLED` | No | 1 | Set to anything other than `1` or `true` to disable the admin dashboard and user impersonation on this instance. |
187
188
  | `EVENT_LOOP_MONITOR_ENABLED` | No | 1 | Node.js event loop lag monitor. |
188
189
 
189
190
  ## Multi-Provider Object Storage
@@ -140,6 +140,50 @@ To use imperative schedules you need to do two things:
140
140
  1. Define a task in your code using `schedules.task()`.
141
141
  2. Attach 1+ schedules to the task either using the dashboard or the SDK.
142
142
 
143
+ ## Spreading runs with windows
144
+
145
+ By default a schedule runs at its exact cron time. When many schedules share the same cron pattern, such as a daily `0 9 * * *`, they all fire at the same time and load your downstream systems as well as ours. A **window** spreads those runs out: each schedule is assigned a stable time within the window after its cron time, so the load is smoothed while each schedule keeps firing at a predictable, repeatable moment.
146
+
147
+ The assigned time is deterministic. A given schedule always lands at the same offset for a given interval, so runs don't jump around between occurrences. And you can always see exactly when the next run will start.
148
+
149
+ Set a `window` as either:
150
+
151
+ - **An absolute duration** in whole minutes or hours, up to 24 hours: `"30m"`, `"2h"`, `"24h"`. Absolute windows are capped at the next cron time, so a run is never delayed past its following occurrence.
152
+ - **A percentage** of the interval between runs: `"30%"`, `"100%"`. A `"50%"` window on an hourly schedule spreads runs across the first 30 minutes of each hour.
153
+
154
+ Set `"0m"` (or `"0%"`) for no spreading — the run fires at its exact cron time.
155
+
156
+ Declarative schedules set the window on the `cron` object:
157
+
158
+ ```ts
159
+ export const dailyReport = schedules.task({
160
+ id: "daily-report",
161
+ cron: {
162
+ pattern: "0 0 * * *",
163
+ // spread this run across the 30 minutes after midnight
164
+ window: "30m",
165
+ },
166
+ run: async (payload) => {},
167
+ });
168
+ ```
169
+
170
+ Imperative schedules set it when creating or updating a schedule, either in the dashboard form or through the SDK:
171
+
172
+ ```ts
173
+ const createdSchedule = await schedules.create({
174
+ task: dailyReport.id,
175
+ cron: "0 0 * * *",
176
+ window: "30m",
177
+ deduplicationKey: "user_123456-daily-report",
178
+ });
179
+ ```
180
+
181
+ <Note>
182
+ The payload `timestamp` and `upcoming` values are always the nominal cron times, not the assigned
183
+ times. When you retrieve a schedule, `nextRun` is the nominal cron time and `nextRunEffectiveAt`
184
+ is the assigned time the run will actually start.
185
+ </Note>
186
+
143
187
  ## Supported cron syntax
144
188
 
145
189
  ```
@@ -198,6 +242,7 @@ These are the options when creating a schedule:
198
242
  | Task | The id of the task you want to attach to. |
199
243
  | Cron pattern | The schedule in cron format. You can also describe it in natural language and press "Generate" to fill this in. |
200
244
  | Timezone | The timezone the schedule will run in. Defaults to "UTC" |
245
+ | Window | An optional [window](#spreading-runs-with-windows) to spread runs after their cron time, e.g. `30m`, `2h`, or `50%`. |
201
246
  | External id | An optional external id, usually you'd use a userId. |
202
247
  | Deduplication key | An optional deduplication key. If you pass the same value, it will update rather than create. Scoped per project, not per environment. |
203
248
  | Environments | The environments this schedule will run in. |