@trigger.dev/sdk 4.5.12 → 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.
- package/dist/commonjs/v3/ai.d.ts +154 -20
- package/dist/commonjs/v3/ai.js +1241 -387
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +7 -2
- package/dist/commonjs/v3/chat.js +22 -7
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/chat.test.js +13 -4
- package/dist/commonjs/v3/chat.test.js.map +1 -1
- package/dist/commonjs/v3/envvars.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +4 -10
- package/dist/commonjs/v3/sessions.js +73 -47
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/test/mock-chat-agent.js +1 -0
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/v3/test/test-session-handle.js +22 -23
- package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +154 -20
- package/dist/esm/v3/ai.js +1239 -387
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +7 -2
- package/dist/esm/v3/chat.js +22 -7
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/chat.test.js +13 -4
- package/dist/esm/v3/chat.test.js.map +1 -1
- package/dist/esm/v3/envvars.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +4 -10
- package/dist/esm/v3/sessions.js +73 -47
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/test/mock-chat-agent.js +2 -1
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/v3/test/test-session-handle.js +23 -24
- package/dist/esm/v3/test/test-session-handle.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/client-protocol.mdx +8 -3
- package/docs/ai-chat/custom-agents.mdx +181 -46
- package/docs/ai-chat/patterns/recovery-boot.mdx +9 -2
- package/docs/ai-chat/patterns/version-upgrades.mdx +26 -6
- package/docs/ai-chat/pending-messages.mdx +5 -3
- package/docs/ai-chat/reference.mdx +26 -10
- package/docs/ai-chat/types.mdx +5 -1
- package/docs/deployment/atomic-deployment.mdx +12 -0
- package/docs/deployment/overview.mdx +7 -1
- package/docs/deployment/version-skew-protection.mdx +430 -0
- package/docs/github-actions.mdx +33 -5
- package/docs/github-integration.mdx +12 -0
- package/docs/self-hosting/env/webapp.mdx +1 -0
- package/docs/vercel-integration.mdx +43 -9
- package/docs/versioning.mdx +2 -0
- package/package.json +2 -2
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Version upgrades"
|
|
3
3
|
sidebarTitle: "Version upgrades"
|
|
4
|
-
description: "Gracefully migrate
|
|
4
|
+
description: "Gracefully migrate chat agents to a new deployment using chat.requestUpgrade(), chat.endAndContinue(), and the continuation mechanism."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
Chat agent runs are pinned to the worker version they started on. When you deploy a new version, suspended runs resume on the **old** code. If your deploy includes breaking changes (new tools, changed schemas, updated API contracts), this can cause issues.
|
|
8
8
|
|
|
9
|
-
`chat.requestUpgrade()`
|
|
9
|
+
`chat.requestUpgrade()` is the managed upgrade signal for `chat.agent()` and the `chat.createSession()` iterator. Fully hand-rolled custom agents use `chat.endAndContinue()` between turns to immediately hand the Session to a new run.
|
|
10
10
|
|
|
11
11
|
## How it works
|
|
12
12
|
|
|
@@ -151,14 +151,34 @@ export const myChat = chat
|
|
|
151
151
|
|
|
152
152
|
This upgrades on **every** deploy, not just breaking changes. Good for fast-moving projects where you always want the latest code.
|
|
153
153
|
|
|
154
|
-
##
|
|
154
|
+
## Custom agents
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
Use `chat.requestUpgrade()` with `chat.agent()`. With `chat.createSession()`, call `chat.requestUpgrade()`, then advance the iterator once more so it can exit normally. For an immediate handoff, close the iterator before calling `chat.endAndContinue()`. In a fully hand-rolled `chat.customAgent()` task, detach input listeners, persist the completed turn, write its boundary, then call `chat.endAndContinue()` and return immediately:
|
|
157
|
+
|
|
158
|
+
Close a `chat.createSession()` iterator between reads. If `return()` races a `next()` that is already waiting for input, it waits for that read to settle before the handoff can continue. Input dispatched while the iterator is closing is not yielded as a turn and remains available to the continuation unless you write another turn-complete boundary.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// Detach any chat.messages.on() subscriptions you created.
|
|
162
|
+
stop.cleanup();
|
|
163
|
+
await persistMessages(conversation.uiMessages);
|
|
164
|
+
await chat.writeTurnComplete();
|
|
165
|
+
await chat.endAndContinue();
|
|
166
|
+
return;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The continuation uses the same durable Session and receives `.in` records that the old run has not consumed. It starts on the latest deployed task version unless the Session's trigger configuration sets `lockToVersion`.
|
|
170
|
+
|
|
171
|
+
If input has been dispatched to the old run but should be processed by the continuation, detach the old listeners and skip the final `chat.writeTurnComplete()`. A turn-complete boundary acknowledges the latest input dispatched to the old run, so writing one after that dispatch would cause the continuation to resume past the input.
|
|
172
|
+
|
|
173
|
+
<Warning>
|
|
174
|
+
`chat.endAndContinue()` starts the successor but does not stop the calling run. Perform no more Session reads or writes after calling it, and return from the task. If the handoff fails, the promise rejects.
|
|
175
|
+
</Warning>
|
|
158
176
|
|
|
159
177
|
## Interaction with recovery boot
|
|
160
178
|
|
|
161
|
-
`chat.requestUpgrade()` is a
|
|
179
|
+
When `chat.requestUpgrade()` is handled before a turn starts, the SDK immediately hands the Session to a new run, which processes the same input on the latest version. When it is requested during a turn, including through `chat.createSession()`, the current turn finishes and the old run exits; the next input starts the continuation run.
|
|
180
|
+
|
|
181
|
+
Both are graceful exits. [`onRecoveryBoot`](/ai-chat/patterns/recovery-boot) does not fire — the hook is reserved for mid-stream interruptions (cancel, crash, or OOM) where a partial assistant exists on the tail.
|
|
162
182
|
|
|
163
183
|
## See also
|
|
164
184
|
|
|
@@ -10,7 +10,9 @@ When an AI agent is executing tool calls, users may want to send a message that
|
|
|
10
10
|
|
|
11
11
|
By default (without `pendingMessages`), a message sent while the agent is responding never interrupts the in-flight response: it's buffered and processed as its own turn once the current turn completes, with multiple messages running sequentially in arrival order.
|
|
12
12
|
|
|
13
|
-
The `pendingMessages` option enables steering instead, injecting user messages between tool-call steps via the AI SDK's `prepareStep`. Messages that arrive during streaming are queued and injected at the next step boundary.
|
|
13
|
+
The `pendingMessages` option enables steering instead, injecting user messages between tool-call steps via the AI SDK's `prepareStep`. Messages that arrive during streaming are queued and injected at the next step boundary. A message that is not injected becomes the next turn instead, whether that is because `shouldInject` returned `false` or because there were no more step boundaries (single-step response or final text generation). The backend handles that, so no client-side re-send is involved.
|
|
14
|
+
|
|
15
|
+
Injection is what needs wiring: the `pendingMessages` options only reach `streamText` if you spread `chat.toStreamTextOptions()` (or pass `prepareStep`). Without that, nothing injects, so every mid-turn message is answered as the next turn. Deferral does not depend on it.
|
|
14
16
|
|
|
15
17
|
## How it works
|
|
16
18
|
|
|
@@ -20,7 +22,7 @@ The `pendingMessages` option enables steering instead, injecting user messages b
|
|
|
20
22
|
4. At the next `prepareStep` boundary (between tool-call steps), `shouldInject` is called
|
|
21
23
|
5. If it returns `true`, the message is injected into the LLM's context
|
|
22
24
|
6. A `data-pending-message-injected` stream chunk confirms injection to the frontend
|
|
23
|
-
7. If `prepareStep` never fires (no tool calls), the message
|
|
25
|
+
7. If `shouldInject` returns `false`, or `prepareStep` never fires (no tool calls), the message stays queued on the backend and is answered as the next turn
|
|
24
26
|
|
|
25
27
|
## Backend: chat.agent
|
|
26
28
|
|
|
@@ -310,7 +312,7 @@ function Chat({ chatId }: { chatId: string }) {
|
|
|
310
312
|
|
|
311
313
|
### Message lifecycle
|
|
312
314
|
|
|
313
|
-
- **Steering messages** are sent via `transport.sendPendingMessage()` immediately. They appear as purple pending bubbles. If injected, they disappear from the overlay and render inline at the injection point. If not injected
|
|
315
|
+
- **Steering messages** are sent via `transport.sendPendingMessage()` immediately. They appear as purple pending bubbles. If injected, they disappear from the overlay and render inline at the injection point. If not injected, the backend answers them as the next turn once the response finishes; the client does not need to re-send them.
|
|
314
316
|
|
|
315
317
|
- **Queued messages** stay client-side until the turn completes, then auto-send as the next turn via `sendMessage()`. They can be promoted to steering mid-stream by clicking "Steer instead".
|
|
316
318
|
|
|
@@ -396,7 +396,7 @@ Options for the `pendingMessages` field. See [Pending Messages](/ai-chat/pending
|
|
|
396
396
|
|
|
397
397
|
| Option | Type | Required | Description |
|
|
398
398
|
| -------------- | --------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------- |
|
|
399
|
-
| `shouldInject` | `(event: PendingMessagesBatchEvent) => boolean \| Promise<boolean>` | No | Decide whether to inject the batch between tool-call steps. If absent,
|
|
399
|
+
| `shouldInject` | `(event: PendingMessagesBatchEvent) => boolean \| Promise<boolean>` | No | Decide whether to inject the batch between tool-call steps. If absent, nothing is injected and the messages are answered as the next turn. Only consulted when `chat.toStreamTextOptions()` (or `prepareStep`) reaches `streamText`; without that nothing is injected and every mid-turn message becomes the next turn. |
|
|
400
400
|
| `prepare` | `(event: PendingMessagesBatchEvent) => ModelMessage[] \| Promise<ModelMessage[]>` | No | Transform the batch before injection. Default: convert each via `convertToModelMessages`. |
|
|
401
401
|
| `onReceived` | `(event: PendingMessageReceivedEvent) => void \| Promise<void>` | No | Called when a message arrives during streaming (per-message). |
|
|
402
402
|
| `onInjected` | `(event: PendingMessagesInjectedEvent) => void \| Promise<void>` | No | Called after a batch is injected via prepareStep. |
|
|
@@ -504,13 +504,14 @@ All methods available on the `chat` object from `@trigger.dev/sdk/ai`.
|
|
|
504
504
|
| `chat.createSession(payload, options)` | Create an async iterator for chat turns |
|
|
505
505
|
| `chat.pipe(source, options?)` | Pipe a stream to the frontend (from anywhere inside a task) |
|
|
506
506
|
| `chat.pipeAndCapture(source, options?)` | Pipe and capture the response; returns `{ message, status, error }` |
|
|
507
|
-
| `chat.writeTurnComplete(options?)` | Signal turn complete; returns `{ lastEventId, sessionInEventId }` resume cursors
|
|
507
|
+
| `chat.writeTurnComplete(options?)` | Signal turn complete; returns `{ lastEventId, sessionInEventId }` resume cursors. `sessionInEventId` is a lower bound, not the sequence of the record the turn answered |
|
|
508
508
|
| `chat.createStopSignal()` | Create a managed stop signal wired to the stop input stream |
|
|
509
|
-
| `chat.messages` |
|
|
509
|
+
| `chat.messages` | Incoming message mailbox; supports non-consuming `.peek()` / `.hasPending()`, single-record `.next()`, `.on()`, and suspend-aware `.waitWithIdleTimeout()` |
|
|
510
510
|
| `chat.local<T>({ id })` | Create a per-run typed local (see [`chat.local`](/ai-chat/chat-local)) |
|
|
511
511
|
| `chat.createStartSessionAction(taskId, options?)` | Returns a server action that creates a chat Session + triggers the first run + returns a session-scoped PAT. Idempotent on `(env, externalId)`. |
|
|
512
512
|
| `chat.waitForHandover(options)` | Wait for a [`chat.headStart`](/ai-chat/fast-starts#handover-with-custom-agents) handover signal in a custom loop. Returns the signal or `null`. `chat.MessageAccumulator` wraps this as `consumeHandover()` / `applyHandover()` |
|
|
513
513
|
| `chat.requestUpgrade()` | End the current run after this turn so the next message starts on the latest agent version. Server-orchestrated handoff. |
|
|
514
|
+
| `chat.endAndContinue()` | In a hand-rolled custom agent, hand off the Session to a fresh continuation run. Call between turns after detaching input listeners, then return immediately. The promise rejects if the handoff fails. |
|
|
514
515
|
| `chat.setTurnTimeout(duration)` | Override turn timeout at runtime (e.g. `"2h"`) |
|
|
515
516
|
| `chat.setTurnTimeoutInSeconds(seconds)` | Override turn timeout at runtime (in seconds) |
|
|
516
517
|
| `chat.setIdleTimeoutInSeconds(seconds)` | Override idle timeout at runtime |
|
|
@@ -546,15 +547,30 @@ Use this when you need [`InferChatUIMessage`](#inferchatuimessage) / typed `data
|
|
|
546
547
|
|
|
547
548
|
## `chat.withClientData`
|
|
548
549
|
|
|
549
|
-
Returns a [`ChatBuilder`](/ai-chat/types#chatbuilder) with a fixed client data schema.
|
|
550
|
+
Returns a [`ChatBuilder`](/ai-chat/types#chatbuilder) with a fixed client data schema. Managed-agent hooks and `run` get typed `clientData` without passing `clientDataSchema` in `.agent()` options. Custom agents parse `payload.metadata` on the initial payload and later input frames before passing it to user code.
|
|
550
551
|
|
|
551
552
|
```ts
|
|
552
|
-
chat.withClientData<TSchema>(
|
|
553
|
+
chat.withClientData<TSchema extends TaskSchema>(config: {
|
|
554
|
+
schema: TSchema;
|
|
555
|
+
reportErrorAt?: "turn-end" | "arrival";
|
|
556
|
+
onValidationError?: (event: {
|
|
557
|
+
error: unknown;
|
|
558
|
+
payload: ChatTaskWirePayload;
|
|
559
|
+
}) => Promise<void> | void;
|
|
560
|
+
}): ChatBuilder<UIMessage, TSchema>;
|
|
553
561
|
```
|
|
554
562
|
|
|
555
|
-
| Parameter
|
|
556
|
-
|
|
|
557
|
-
| `schema`
|
|
563
|
+
| Parameter | Type | Default | Description |
|
|
564
|
+
| ------------------- | --------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------ |
|
|
565
|
+
| `schema` | `TaskSchema` | required | Zod, ArkType, Valibot, or any supported schema lib |
|
|
566
|
+
| `reportErrorAt` | `"turn-end" \| "arrival"` | `"turn-end"` | When the client-visible `Invalid client data` error is written for a steering frame that failed validation mid-turn |
|
|
567
|
+
| `onValidationError` | `(event) => void` | — | Called when an input fails validation. Composes with the task-level `onClientDataValidationError` rather than replacing it |
|
|
568
|
+
|
|
569
|
+
`reportErrorAt` governs only the stream-visible error, and only for frames arriving on the steering subscription created by `chat.createSession({ pendingMessages })`. `"turn-end"` holds it until the turn closes, so a bad send cannot truncate an answer the user is already reading; `"arrival"` writes it as soon as validation fails, ending the response in progress. `onValidationError` and the task log fire on arrival in both modes, and the frame is never delivered as a turn either way. A raw `chat.messages.on()` subscription has no turn boundary to defer to, so it always reports through the callback and the task log without writing to the stream.
|
|
570
|
+
|
|
571
|
+
For `chat.customAgent()`, invalid client data is skipped. Async reads emit an error chunk followed by `turn-complete`. A `chat.messages.on()` subscription uses the task's `onClientDataValidationError` callback and task log instead, so an active response is not ended early. Without a schema, metadata is passed through unchanged.
|
|
572
|
+
|
|
573
|
+
Passing options directly to `chat.customAgent()` instead of through the builder uses the flat equivalents: `clientDataSchema`, `clientDataReportErrorAt`, and `onClientDataValidationError`.
|
|
558
574
|
|
|
559
575
|
Full guide: [Typed client data](/ai-chat/types#typed-client-data-with-chatwithclientdata).
|
|
560
576
|
|
|
@@ -645,7 +661,7 @@ The `onEvent` callback receives a `ChatTransportEvent` (exported from `@trigger.
|
|
|
645
661
|
| `message-send-failed` | `messageId?`, `source`, `error`, `status?`, `durationMs`, `partId?`, `bodyBytes?` | A send definitively failed after internal retries. Fires in addition to `useChat`'s `onError`. |
|
|
646
662
|
| `stream-connected` | `resumed`, `lastEventId?`, `messageId?` | The SSE subscription to the session's output stream started delivering. `resumed: true` when reconnecting from a stored cursor (page reload) rather than following a fresh send. `lastEventId` is the cursor it connected from. |
|
|
647
663
|
| `first-chunk` | `chunkType?`, `lastEventId?`, `messageId?`, `sinceSendMs?` | The first response chunk of a turn arrived. `sinceSendMs` is the delta from the last turn-producing send — time to first token without any bookkeeping. |
|
|
648
|
-
| `turn-completed` | `lastEventId?`, `sessionInEventId?`, `messageId?`, `sinceSendMs?` | The agent's turn-complete control record arrived — the "finished answering" signal. `sinceSendMs` is the full turn latency; `sessionInEventId` is the agent
|
|
664
|
+
| `turn-completed` | `lastEventId?`, `sessionInEventId?`, `messageId?`, `sinceSendMs?` | The agent's turn-complete control record arrived — the "finished answering" signal. `sinceSendMs` is the full turn latency; `sessionInEventId` is the cursor the agent can safely resume its input stream from. Treat it as a lower bound: it is held back behind any message still waiting to be handled, so it can be below the sequence of the record this turn answered. Do not use it to decide whether a turn boundary belongs to your own send. |
|
|
649
665
|
| `stream-error` | `error`, `status?` | The output stream failed unrecoverably. |
|
|
650
666
|
|
|
651
667
|
`source` identifies the send path: `"submit-message"`, `"regenerate-message"`, `"steer"` (`sendPendingMessage`), `"action"` (`sendAction`), `"stop"` (`stopGeneration`), or `"head-start"`.
|
|
@@ -784,7 +800,7 @@ Send a custom action to the agent. Actions wake the agent from suspension and fi
|
|
|
784
800
|
transport.sendAction(chatId: string, action: unknown): Promise<ReadableStream<UIMessageChunk>>
|
|
785
801
|
```
|
|
786
802
|
|
|
787
|
-
|
|
803
|
+
For managed `chat.agent()` tasks, the action payload is validated against the agent's `actionSchema` on the backend. Raw `chat.customAgent()` tasks receive it as `unknown` and must validate it themselves.
|
|
788
804
|
|
|
789
805
|
```tsx
|
|
790
806
|
// Undo button
|
package/docs/ai-chat/types.mdx
CHANGED
|
@@ -140,7 +140,7 @@ You can also import `InferChatUIMessage` from `@trigger.dev/sdk/ai` in non-React
|
|
|
140
140
|
|
|
141
141
|
## Typed client data with `chat.withClientData`
|
|
142
142
|
|
|
143
|
-
`chat.withClientData({ schema })` returns a [ChatBuilder](#chatbuilder) that fixes the client data schema.
|
|
143
|
+
`chat.withClientData({ schema })` returns a [ChatBuilder](#chatbuilder) that fixes the client data schema. Managed-agent hooks and `run` receive typed `clientData` without needing `clientDataSchema` in `.agent()` options. A `.customAgent()` run receives the parsed schema output in `payload.metadata`, and `chat.createSession()` yields it as `turn.clientData`.
|
|
144
144
|
|
|
145
145
|
```ts
|
|
146
146
|
import { chat } from "@trigger.dev/sdk/ai";
|
|
@@ -167,6 +167,10 @@ export const myChat = chat
|
|
|
167
167
|
});
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
The schema runs at runtime for both `.agent()` and `.customAgent()`. Custom agents validate the initial payload and later `chat.messages` frames. Invalid frames are not passed to user code. Async reads emit an error chunk followed by `turn-complete`; `chat.messages.on()` reports through `onClientDataValidationError` and the task log so it does not end an active response. Without a schema, metadata is passed through unchanged.
|
|
171
|
+
|
|
172
|
+
`withClientData` also takes `reportErrorAt` and `onValidationError` alongside `schema`. See [chat.withClientData](/ai-chat/reference#chatwithclientdata) for both, and [Validating client data](/ai-chat/custom-agents#validating-client-data) for the custom-agent walkthrough.
|
|
173
|
+
|
|
170
174
|
## ChatBuilder
|
|
171
175
|
|
|
172
176
|
Both `chat.withUIMessage()` and `chat.withClientData()` return a **ChatBuilder** — a chainable object that accumulates configuration before creating the agent with `.agent()`.
|
|
@@ -4,6 +4,18 @@ sidebarTitle: "Atomic deploys"
|
|
|
4
4
|
description: "Use atomic deploys to coordinate changes to your tasks and your application."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
<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.
|
|
17
|
+
</Warning>
|
|
18
|
+
|
|
7
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.
|
|
8
20
|
|
|
9
21
|
## How it works
|
|
@@ -122,6 +122,12 @@ If you want to set a global version to run all tasks against, you can use the `T
|
|
|
122
122
|
TRIGGER_VERSION=20250228.1
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
<Tip>
|
|
126
|
+
If what you actually want is for each release of your app to run against the tasks built from the
|
|
127
|
+
same commit, you don't need to plumb version numbers around by hand. See [version skew
|
|
128
|
+
protection](/deployment/version-skew-protection).
|
|
129
|
+
</Tip>
|
|
130
|
+
|
|
125
131
|
### Child tasks and auto-version locking
|
|
126
132
|
|
|
127
133
|
Trigger and wait functions version lock child task runs to the parent task run version. This ensures the results from child runs match what the parent task is expecting. If you don't wait then version locking doesn't apply.
|
|
@@ -155,7 +161,7 @@ Or from the dashboard:
|
|
|
155
161
|
|
|
156
162
|

|
|
157
163
|
|
|
158
|
-
To learn more about skipping promotion and how this enables atomic deployments, see our [Atomic deployment](/deployment/atomic-deployment) guide.
|
|
164
|
+
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).
|
|
159
165
|
|
|
160
166
|
## Staging deploys
|
|
161
167
|
|