Skip to content

Commit 18df18c

Browse files
committed
feat(sdk): allow opting out of TRIGGER_VERSION locking via version: null
Closes #3380. `TriggerOptions.version` is extended from `string` to `string | null`, and `ApiClientConfiguration` gains a new `version?: string | null` field that applies to every trigger inside an `auth.withAuth(...)` scope. Passing `null` omits `lockToVersion` from the request so the server resolves to the current deployed version, ignoring the `TRIGGER_VERSION` env var. Motivating use case: a single Node.js process triggers tasks in two Trigger.dev projects (e.g. a main project connected to the Vercel integration plus a sibling project for autonomous agents). The ambient `TRIGGER_VERSION` is scoped to the main project, and the SDK previously had no way to opt cross-project calls out of that pin. Resolution precedence (highest first): 1. per-call `options.version` 2. scoped `ApiClientConfiguration.version` 3. `TRIGGER_VERSION` env var `undefined` at any level falls through; only `null` explicitly unpins. All 7 trigger paths in `shared.ts` route through the new `apiClientManager.resolveLockToVersion(...)` helper. The parent-run `lockToVersion: taskContext.worker?.version` paths (triggerAndWait / batchTriggerAndWait / child tasks) are unchanged by design.
1 parent 73ea586 commit 18df18c

6 files changed

Lines changed: 195 additions & 10 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@trigger.dev/core": minor
3+
"@trigger.dev/sdk": minor
4+
---
5+
6+
Allow opting out of `TRIGGER_VERSION` locking per-call and per-scope (fixes #3380).
7+
8+
`TriggerOptions.version` now accepts `null` in addition to a version string, and `ApiClientConfiguration` gains a `version?: string | null` field that applies to every trigger inside an `auth.withAuth(...)` scope. Passing `null` explicitly unpins the call: `lockToVersion` is omitted from the request and the server resolves to the current deployed version, ignoring the `TRIGGER_VERSION` environment variable.
9+
10+
Precedence (highest first): per-call `version` option, scoped `version` in `ApiClientConfiguration`, `TRIGGER_VERSION` env var. `undefined` at any level falls through to the next level; only `null` explicitly unpins.
11+
12+
Use cases:
13+
- Cross-project triggers where the ambient `TRIGGER_VERSION` (e.g., injected by the Vercel integration for your "main" project) does not apply to a sibling project.
14+
- One-off calls that should always run on the current deployed version regardless of the runtime environment.
15+
16+
```ts
17+
// Scoped: every trigger inside this scope resolves to the current deployed version
18+
await auth.withAuth({ secretKey, version: null }, async () => {
19+
await tasks.trigger("some-task", payload);
20+
});
21+
22+
// Per-call: only this call is unpinned
23+
await tasks.trigger("some-task", payload, { version: null });
24+
```
25+
26+
The existing string-pin behavior and `TRIGGER_VERSION` fallback are unchanged.

packages/core/src/v3/apiClientManager/index.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,13 @@ export class APIClientManagerAPI {
6262
const requestOptions = this.#getConfig()?.requestOptions;
6363
const futureFlags = this.#getConfig()?.future;
6464

65-
return new ApiClient(this.baseURL, this.accessToken, this.branchName, requestOptions, futureFlags);
65+
return new ApiClient(
66+
this.baseURL,
67+
this.accessToken,
68+
this.branchName,
69+
requestOptions,
70+
futureFlags
71+
);
6672
}
6773

6874
clientOrThrow(config?: ApiClientConfiguration): ApiClient {
@@ -80,6 +86,32 @@ export class APIClientManagerAPI {
8086
return new ApiClient(baseURL, accessToken, branchName, requestOptions, futureFlags);
8187
}
8288

89+
/**
90+
* Resolves the value to send as `lockToVersion` on a trigger request.
91+
*
92+
* Precedence (highest first):
93+
* 1. Per-call `version` option (a string pins; `null` explicitly unpins).
94+
* 2. Scoped `version` in `ApiClientConfiguration` (via `runWithConfig` / `auth.withAuth`).
95+
* 3. `TRIGGER_VERSION` environment variable.
96+
*
97+
* Returns `undefined` when the result should not be sent (unpinned, server resolves
98+
* to the current deployed version). Returns a version string when the request should
99+
* be pinned to that specific version. `undefined` at any level falls through to the
100+
* next level; only `null` explicitly unpins.
101+
*/
102+
resolveLockToVersion(callVersion?: string | null): string | undefined {
103+
if (callVersion !== undefined) {
104+
return callVersion === null ? undefined : callVersion;
105+
}
106+
107+
const scopedVersion = this.#getConfig()?.version;
108+
if (scopedVersion !== undefined) {
109+
return scopedVersion === null ? undefined : scopedVersion;
110+
}
111+
112+
return getEnvVar("TRIGGER_VERSION");
113+
}
114+
83115
runWithConfig<R extends (...args: any[]) => Promise<any>>(
84116
config: ApiClientConfiguration,
85117
fn: R

packages/core/src/v3/apiClientManager/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ export type ApiClientConfiguration = {
1414
* The preview branch name (for preview environments)
1515
*/
1616
previewBranch?: string;
17+
/**
18+
* Controls the `lockToVersion` applied to task triggers in this scope.
19+
*
20+
* - A version string (e.g. `"20250208.1"`) pins every trigger in the scope to that version.
21+
* - `null` explicitly unpins: `lockToVersion` is omitted from the request and the server
22+
* resolves to the current deployed version. Ignores the `TRIGGER_VERSION` environment
23+
* variable. Use this when triggering into a project where the ambient `TRIGGER_VERSION`
24+
* does not apply (for example, cross-project triggers).
25+
* - Omitted (`undefined`) preserves the default behavior: per-call `version` option, then
26+
* the `TRIGGER_VERSION` environment variable.
27+
*
28+
* A per-call `TriggerOptions.version` always wins over this scoped value.
29+
*/
30+
version?: string | null;
1731
requestOptions?: ApiRequestOptions;
1832
future?: ApiClientFutureFlags;
1933
};

packages/core/src/v3/types/tasks.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -895,16 +895,25 @@ export type TriggerOptions = {
895895
* but you can specify a specific version to run here. You can also set the TRIGGER_VERSION environment
896896
* variables to run a specific version for all tasks.
897897
*
898+
* Pass `null` to explicitly unpin this call: `lockToVersion` is omitted from the request and the
899+
* server resolves to the current deployed version, ignoring the `TRIGGER_VERSION` environment
900+
* variable. Useful when triggering into a project where the ambient `TRIGGER_VERSION` does not
901+
* apply (for example, cross-project triggers).
902+
*
898903
* @example
899904
*
900905
* ```ts
906+
* // Pin to a specific version
901907
* await myTask.trigger({ foo: "bar" }, { version: "20250208.1" });
908+
*
909+
* // Explicitly use the current deployed version, ignoring TRIGGER_VERSION
910+
* await myTask.trigger({ foo: "bar" }, { version: null });
902911
* ```
903912
*
904913
* Note that this option is only available for `trigger` and NOT `triggerAndWait` (and their batch counterparts). The "wait" versions will always be locked
905914
* to the same version as the parent task that is triggering the child tasks.
906915
*/
907-
version?: string;
916+
version?: string | null;
908917

909918
/**
910919
* Specify the region to run the task in. This overrides the default region set for your project in the dashboard.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { apiClientManager } from "../src/v3/apiClientManager-api.js";
2+
3+
const originalEnv = process.env.TRIGGER_VERSION;
4+
5+
describe("APIClientManagerAPI.resolveLockToVersion", () => {
6+
beforeEach(() => {
7+
delete process.env.TRIGGER_VERSION;
8+
});
9+
10+
afterEach(() => {
11+
if (originalEnv === undefined) {
12+
delete process.env.TRIGGER_VERSION;
13+
} else {
14+
process.env.TRIGGER_VERSION = originalEnv;
15+
}
16+
apiClientManager.disable();
17+
});
18+
19+
describe("without a scope override", () => {
20+
it("returns undefined when no call version is given and TRIGGER_VERSION is unset", () => {
21+
expect(apiClientManager.resolveLockToVersion()).toBeUndefined();
22+
});
23+
24+
it("falls back to TRIGGER_VERSION when no call version is given", () => {
25+
process.env.TRIGGER_VERSION = "20250101.1";
26+
expect(apiClientManager.resolveLockToVersion()).toBe("20250101.1");
27+
});
28+
29+
it("prefers a per-call version string over TRIGGER_VERSION", () => {
30+
process.env.TRIGGER_VERSION = "20250101.1";
31+
expect(apiClientManager.resolveLockToVersion("20250202.1")).toBe("20250202.1");
32+
});
33+
34+
it("returns undefined when per-call version is null, even if TRIGGER_VERSION is set", () => {
35+
process.env.TRIGGER_VERSION = "20250101.1";
36+
expect(apiClientManager.resolveLockToVersion(null)).toBeUndefined();
37+
});
38+
});
39+
40+
describe("inside a scope with a version string", () => {
41+
it("uses the scoped version when no call version is given", async () => {
42+
process.env.TRIGGER_VERSION = "20250101.1";
43+
44+
await apiClientManager.runWithConfig({ version: "20250303.1" }, async () => {
45+
expect(apiClientManager.resolveLockToVersion()).toBe("20250303.1");
46+
});
47+
});
48+
49+
it("lets a per-call version string win over the scope", async () => {
50+
await apiClientManager.runWithConfig({ version: "20250303.1" }, async () => {
51+
expect(apiClientManager.resolveLockToVersion("20250404.1")).toBe("20250404.1");
52+
});
53+
});
54+
55+
it("lets a per-call null win over the scope", async () => {
56+
await apiClientManager.runWithConfig({ version: "20250303.1" }, async () => {
57+
expect(apiClientManager.resolveLockToVersion(null)).toBeUndefined();
58+
});
59+
});
60+
});
61+
62+
describe("inside a scope with version: null", () => {
63+
it("ignores TRIGGER_VERSION when no call version is given", async () => {
64+
process.env.TRIGGER_VERSION = "20250101.1";
65+
66+
await apiClientManager.runWithConfig({ version: null }, async () => {
67+
expect(apiClientManager.resolveLockToVersion()).toBeUndefined();
68+
});
69+
});
70+
71+
it("lets a per-call version string win over the null scope", async () => {
72+
await apiClientManager.runWithConfig({ version: null }, async () => {
73+
expect(apiClientManager.resolveLockToVersion("20250505.1")).toBe("20250505.1");
74+
});
75+
});
76+
});
77+
78+
describe("scope without a version key", () => {
79+
it("falls back to TRIGGER_VERSION", async () => {
80+
process.env.TRIGGER_VERSION = "20250101.1";
81+
82+
await apiClientManager.runWithConfig({ accessToken: "tr_test_xyz" }, async () => {
83+
expect(apiClientManager.resolveLockToVersion()).toBe("20250101.1");
84+
});
85+
});
86+
87+
it("still respects a per-call null", async () => {
88+
process.env.TRIGGER_VERSION = "20250101.1";
89+
90+
await apiClientManager.runWithConfig({ accessToken: "tr_test_xyz" }, async () => {
91+
expect(apiClientManager.resolveLockToVersion(null)).toBeUndefined();
92+
});
93+
});
94+
});
95+
96+
describe("scope with version: undefined explicitly", () => {
97+
it("treats explicit undefined as 'no key' and falls back to TRIGGER_VERSION", async () => {
98+
process.env.TRIGGER_VERSION = "20250101.1";
99+
100+
await apiClientManager.runWithConfig({ version: undefined }, async () => {
101+
expect(apiClientManager.resolveLockToVersion()).toBe("20250101.1");
102+
});
103+
});
104+
});
105+
});

packages/trigger-sdk/src/v3/shared.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
createErrorTaskError,
1111
defaultRetryOptions,
1212
flattenIdempotencyKey,
13-
getEnvVar,
1413
getIdempotencyKeyOptions,
1514
getSchemaParseFn,
1615
InitOutput,
@@ -650,7 +649,7 @@ export async function batchTriggerById<TTask extends AnyTask>(
650649
machine: item.options?.machine,
651650
priority: item.options?.priority,
652651
region: item.options?.region,
653-
lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
652+
lockToVersion: apiClientManager.resolveLockToVersion(item.options?.version),
654653
debounce: item.options?.debounce,
655654
},
656655
};
@@ -1166,7 +1165,7 @@ export async function batchTriggerTasks<TTasks extends readonly AnyTask[]>(
11661165
machine: item.options?.machine,
11671166
priority: item.options?.priority,
11681167
region: item.options?.region,
1169-
lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
1168+
lockToVersion: apiClientManager.resolveLockToVersion(item.options?.version),
11701169
debounce: item.options?.debounce,
11711170
},
11721171
};
@@ -1830,7 +1829,7 @@ async function* transformBatchItemsStream<TTask extends AnyTask>(
18301829
machine: item.options?.machine,
18311830
priority: item.options?.priority,
18321831
region: item.options?.region,
1833-
lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
1832+
lockToVersion: apiClientManager.resolveLockToVersion(item.options?.version),
18341833
debounce: item.options?.debounce,
18351834
},
18361835
};
@@ -1933,7 +1932,7 @@ async function* transformBatchByTaskItemsStream<TTasks extends readonly AnyTask[
19331932
machine: item.options?.machine,
19341933
priority: item.options?.priority,
19351934
region: item.options?.region,
1936-
lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
1935+
lockToVersion: apiClientManager.resolveLockToVersion(item.options?.version),
19371936
debounce: item.options?.debounce,
19381937
},
19391938
};
@@ -2037,7 +2036,7 @@ async function* transformSingleTaskBatchItemsStream<TPayload>(
20372036
machine: item.options?.machine,
20382037
priority: item.options?.priority,
20392038
region: item.options?.region,
2040-
lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
2039+
lockToVersion: apiClientManager.resolveLockToVersion(item.options?.version),
20412040
debounce: item.options?.debounce,
20422041
},
20432042
};
@@ -2146,7 +2145,7 @@ async function trigger_internal<TRunTypes extends AnyRunTypes>(
21462145
machine: options?.machine,
21472146
priority: options?.priority,
21482147
region: options?.region,
2149-
lockToVersion: options?.version ?? getEnvVar("TRIGGER_VERSION"),
2148+
lockToVersion: apiClientManager.resolveLockToVersion(options?.version),
21502149
debounce: options?.debounce,
21512150
},
21522151
},
@@ -2232,7 +2231,7 @@ async function batchTrigger_internal<TRunTypes extends AnyRunTypes>(
22322231
machine: item.options?.machine,
22332232
priority: item.options?.priority,
22342233
region: item.options?.region,
2235-
lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
2234+
lockToVersion: apiClientManager.resolveLockToVersion(item.options?.version),
22362235
},
22372236
};
22382237
})

0 commit comments

Comments
 (0)