Skip to content
Open
18 changes: 18 additions & 0 deletions .changeset/trigger-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@trigger.dev/sdk": patch
---

Add `TriggerClient` for running multiple SDK clients side-by-side, each with its own auth, preview branch, and baseURL. Useful when a single process needs to trigger tasks or read runs across multiple projects, environments, or preview branches without mutating shared global state.

```ts
import { TriggerClient } from "@trigger.dev/sdk";

const prod = new TriggerClient({ accessToken: process.env.TRIGGER_PROD_KEY });
const preview = new TriggerClient({
accessToken: process.env.TRIGGER_PREVIEW_KEY,
previewBranch: "signup-flow",
});

await prod.tasks.trigger("send-email", payload);
await preview.runs.list({ status: ["COMPLETED"] });
```
23 changes: 21 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"./v3/runEngineWorker": "./src/v3/runEngineWorker/index.ts",
"./v3/machines": "./src/v3/machines/index.ts",
"./v3/serverOnly": "./src/v3/serverOnly/index.ts",
"./v3/isomorphic": "./src/v3/isomorphic/index.ts"
"./v3/isomorphic": "./src/v3/isomorphic/index.ts",
"./v3/sdk-scope-storage": "./src/v3/sdkScope/storage-node.ts"
},
"sourceDialects": [
"@triggerdotdev/source"
Expand Down Expand Up @@ -162,12 +163,19 @@
"v3/isomorphic": [
"dist/commonjs/v3/isomorphic/index.d.ts"
],
"v3/sdk-scope-storage": [
"dist/commonjs/v3/sdkScope/storage-node.d.ts"
],
"v3/test": [
"dist/commonjs/v3/test/index.d.ts"
]
}
},
"sideEffects": false,
"sideEffects": [
"./dist/esm/v3/sdkScope/storage-node.js",
"./dist/commonjs/v3/sdkScope/storage-node.js",
"./src/v3/sdkScope/storage-node.ts"
],
"scripts": {
"clean": "rimraf dist .tshy .tshy-build .turbo src/v3/vendor",
"update-version": "tsx ../../scripts/updateVersion.ts",
Expand Down Expand Up @@ -622,6 +630,17 @@
"types": "./dist/commonjs/v3/isomorphic/index.d.ts",
"default": "./dist/commonjs/v3/isomorphic/index.js"
}
},
"./v3/sdk-scope-storage": {
"import": {
"@triggerdotdev/source": "./src/v3/sdkScope/storage-node.ts",
"types": "./dist/esm/v3/sdkScope/storage-node.d.ts",
"default": "./dist/esm/v3/sdkScope/storage-node.js"
},
"require": {
"types": "./dist/commonjs/v3/sdkScope/storage-node.d.ts",
"default": "./dist/commonjs/v3/sdkScope/storage-node.js"
}
}
},
"type": "module",
Expand Down
61 changes: 52 additions & 9 deletions packages/core/src/v3/apiClientManager/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ApiClient } from "../apiClient/index.js";
import { getGlobal, registerGlobal, unregisterGlobal } from "../utils/globals.js";
import { getEnvVar } from "../utils/getEnv.js";
import { sdkScope } from "../sdkScope/index.js";
import { ApiClientConfiguration } from "./types.js";

const API_NAME = "api-client";
Expand Down Expand Up @@ -30,11 +31,19 @@ export class APIClientManagerAPI {
}

get baseURL(): string | undefined {
const scoped = sdkScope.getStore();
if (scoped) {
return scoped.apiClientConfig.baseURL ?? "https://api.trigger.dev";
}
const config = this.#getConfig();
return config?.baseURL ?? getEnvVar("TRIGGER_API_URL") ?? "https://api.trigger.dev";
}

get accessToken(): string | undefined {
const scoped = sdkScope.getStore();
if (scoped) {
return scoped.apiClientConfig.accessToken ?? scoped.apiClientConfig.secretKey;
}
Comment thread
ericallam marked this conversation as resolved.
const config = this.#getConfig();
return (
config?.secretKey ??
Expand All @@ -45,6 +54,11 @@ export class APIClientManagerAPI {
}

get branchName(): string | undefined {
const scoped = sdkScope.getStore();
if (scoped) {
const value = scoped.apiClientConfig.previewBranch;
return value ? value : undefined;
}
const config = this.#getConfig();
const value =
config?.previewBranch ??
Expand All @@ -54,13 +68,33 @@ export class APIClientManagerAPI {
return value ? value : undefined;
}

public resolveApiClientConfig(partial: ApiClientConfiguration = {}): ApiClientConfiguration {
return {
baseURL: partial.baseURL ?? getEnvVar("TRIGGER_API_URL"),
accessToken:
partial.accessToken ??
partial.secretKey ??
getEnvVar("TRIGGER_SECRET_KEY") ??
getEnvVar("TRIGGER_ACCESS_TOKEN"),
secretKey: partial.secretKey,
previewBranch:
partial.previewBranch ??
getEnvVar("TRIGGER_PREVIEW_BRANCH") ??
getEnvVar("VERCEL_GIT_COMMIT_REF"),
requestOptions: partial.requestOptions,
future: partial.future,
};
}

get client(): ApiClient | undefined {
if (!this.baseURL || !this.accessToken) {
return undefined;
}

const requestOptions = this.#getConfig()?.requestOptions;
const futureFlags = this.#getConfig()?.future;
const scoped = sdkScope.getStore();
const source = scoped?.apiClientConfig ?? this.#getConfig();
const requestOptions = source?.requestOptions;
const futureFlags = source?.future;

return new ApiClient(this.baseURL, this.accessToken, this.branchName, requestOptions, futureFlags);
}
Expand All @@ -74,8 +108,10 @@ export class APIClientManagerAPI {
}

const branchName = config?.previewBranch ?? this.branchName;
const requestOptions = config?.requestOptions ?? this.#getConfig()?.requestOptions;
const futureFlags = config?.future ?? this.#getConfig()?.future;
const scoped = sdkScope.getStore();
const source = scoped?.apiClientConfig ?? this.#getConfig();
const requestOptions = config?.requestOptions ?? source?.requestOptions;
const futureFlags = config?.future ?? source?.future;

return new ApiClient(baseURL, accessToken, branchName, requestOptions, futureFlags);
}
Expand All @@ -84,17 +120,24 @@ export class APIClientManagerAPI {
config: ApiClientConfiguration,
fn: R
): Promise<ReturnType<R>> {
const originalConfig = this.#getConfig();
const $config = { ...originalConfig, ...config };
registerGlobal(API_NAME, $config, true);
const current = sdkScope.getStore()?.apiClientConfig ?? this.#getConfig();
const merged = this.resolveApiClientConfig({ ...current, ...config });

if (sdkScope.hasStorage()) {
return sdkScope.withScope({ apiClientConfig: merged, inheritContext: true }, fn);
}

// No ALS available (browser, edge, workers). Fall back to in-place
// mutation — same as pre-existing behavior, not concurrency-safe.
const original = this.#getConfig();
registerGlobal(API_NAME, merged, true);
return fn().finally(() => {
registerGlobal(API_NAME, originalConfig, true);
registerGlobal(API_NAME, original, true);
});
}

public setGlobalAPIClientConfiguration(config: ApiClientConfiguration): boolean {
return registerGlobal(API_NAME, config);
return registerGlobal(API_NAME, config, true);
}

#getConfig(): ApiClientConfiguration | undefined {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/v3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from "./runtime-api.js";
export * from "./task-context-api.js";
export * from "./trace-context-api.js";
export * from "./apiClientManager-api.js";
export * from "./sdkScope-api.js";
export * from "./usage-api.js";
export * from "./run-metadata-api.js";
export * from "./wait-until-api.js";
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/v3/sdkScope-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { sdkScope, type SdkScope } from "./sdkScope/index.js";
21 changes: 21 additions & 0 deletions packages/core/src/v3/sdkScope/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { SdkScope, SdkScopeStorage } from "./types.js";

export type { SdkScope, SdkScopeStorage } from "./types.js";

let installedStorage: SdkScopeStorage | undefined;

export function _installSdkScopeStorage(storage: SdkScopeStorage): void {
installedStorage = storage;
}

export const sdkScope = {
hasStorage(): boolean {
return installedStorage !== undefined;
},
getStore(): SdkScope | undefined {
return installedStorage?.getStore();
},
withScope<R>(scope: SdkScope, fn: () => R): R {
return installedStorage ? installedStorage.run(scope, fn) : fn();
},
};
10 changes: 10 additions & 0 deletions packages/core/src/v3/sdkScope/storage-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { AsyncLocalStorage } from "node:async_hooks";
import { _installSdkScopeStorage } from "./index.js";
import type { SdkScope } from "./types.js";

const als = new AsyncLocalStorage<SdkScope>();

_installSdkScopeStorage({
getStore: () => als.getStore(),
run: (scope, fn) => als.run(scope, fn),
});
11 changes: 11 additions & 0 deletions packages/core/src/v3/sdkScope/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { ApiClientConfiguration } from "../apiClientManager/types.js";

export type SdkScope = {
apiClientConfig: ApiClientConfiguration;
inheritContext: boolean;
};

export type SdkScopeStorage = {
getStore(): SdkScope | undefined;
run<R>(scope: SdkScope, fn: () => R): R;
};
10 changes: 10 additions & 0 deletions packages/core/src/v3/taskContext/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Attributes } from "@opentelemetry/api";
import { ServerBackgroundWorker, TaskRunContext } from "../schemas/index.js";
import { SemanticInternalAttributes } from "../semanticInternalAttributes.js";
import { sdkScope } from "../sdkScope/index.js";
import { getGlobal, registerGlobal } from "../utils/globals.js";
import { TaskContext } from "./types.js";

Expand All @@ -22,6 +23,7 @@ export class TaskContextAPI {
}

get isInsideTask(): boolean {
if (this.#isolatedFromContext()) return false;
return this.#getTaskContext() !== undefined;
}

Expand All @@ -30,17 +32,25 @@ export class TaskContextAPI {
}

get ctx(): TaskRunContext | undefined {
if (this.#isolatedFromContext()) return undefined;
return this.#getTaskContext()?.ctx;
}

get worker(): ServerBackgroundWorker | undefined {
if (this.#isolatedFromContext()) return undefined;
return this.#getTaskContext()?.worker;
}

get isWarmStart(): boolean | undefined {
if (this.#isolatedFromContext()) return undefined;
return this.#getTaskContext()?.isWarmStart;
}

#isolatedFromContext(): boolean {
const scope = sdkScope.getStore();
return !!scope && !scope.inheritContext;
}

get attributes(): Attributes {
if (this.ctx) {
return {
Expand Down
1 change: 1 addition & 0 deletions packages/trigger-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"directory": "packages/trigger-sdk"
},
"type": "module",
"sideEffects": false,
"files": [
"dist"
],
Expand Down
1 change: 1 addition & 0 deletions packages/trigger-sdk/src/v3/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
RealtimeRunSkipColumns,
} from "@trigger.dev/core/v3";
import { generateJWT as internal_generateJWT } from "@trigger.dev/core/v3";
import "@trigger.dev/core/v3/sdk-scope-storage";

/**
* Register the global API client configuration. Alternatively, you can set the `TRIGGER_SECRET_KEY` and `TRIGGER_API_URL` environment variables.
Expand Down
1 change: 1 addition & 0 deletions packages/trigger-sdk/src/v3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,6 @@ export * as queues from "./queues.js";
export type { ImportEnvironmentVariablesParams } from "./envvars.js";

export { configure, auth } from "./auth.js";
export { TriggerClient, type TriggerClientConfig } from "./triggerClient.js";
export * as prompts from "./prompts.js";
export * as skills from "./skills.js";
21 changes: 14 additions & 7 deletions packages/trigger-sdk/src/v3/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
RateLimitError,
resourceCatalog,
runtime,
sdkScope,
SemanticInternalAttributes,
stringifyIO,
SubtaskUnwrapError,
Expand Down Expand Up @@ -129,6 +130,12 @@ export { SubtaskUnwrapError, TaskRunPromise };

export type Context = TaskRunContext;

function scopedEnvVar(name: string): string | undefined {
const scope = sdkScope.getStore();
if (scope && !scope.inheritContext) return undefined;
return getEnvVar(name);
}

export function queue(options: QueueOptions): Queue {
resourceCatalog.registerQueueMetadata(options);

Expand Down Expand Up @@ -740,7 +747,7 @@ export async function batchTriggerById<TTask extends AnyTask>(
machine: item.options?.machine,
priority: item.options?.priority,
region: item.options?.region,
lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
lockToVersion: item.options?.version ?? scopedEnvVar("TRIGGER_VERSION"),
debounce: item.options?.debounce,
},
};
Expand Down Expand Up @@ -1256,7 +1263,7 @@ export async function batchTriggerTasks<TTasks extends readonly AnyTask[]>(
machine: item.options?.machine,
priority: item.options?.priority,
region: item.options?.region,
lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
lockToVersion: item.options?.version ?? scopedEnvVar("TRIGGER_VERSION"),
debounce: item.options?.debounce,
},
};
Expand Down Expand Up @@ -1920,7 +1927,7 @@ async function* transformBatchItemsStream<TTask extends AnyTask>(
machine: item.options?.machine,
priority: item.options?.priority,
region: item.options?.region,
lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
lockToVersion: item.options?.version ?? scopedEnvVar("TRIGGER_VERSION"),
debounce: item.options?.debounce,
},
};
Expand Down Expand Up @@ -2023,7 +2030,7 @@ async function* transformBatchByTaskItemsStream<TTasks extends readonly AnyTask[
machine: item.options?.machine,
priority: item.options?.priority,
region: item.options?.region,
lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
lockToVersion: item.options?.version ?? scopedEnvVar("TRIGGER_VERSION"),
debounce: item.options?.debounce,
},
};
Expand Down Expand Up @@ -2127,7 +2134,7 @@ async function* transformSingleTaskBatchItemsStream<TPayload>(
machine: item.options?.machine,
priority: item.options?.priority,
region: item.options?.region,
lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
lockToVersion: item.options?.version ?? scopedEnvVar("TRIGGER_VERSION"),
debounce: item.options?.debounce,
},
};
Expand Down Expand Up @@ -2236,7 +2243,7 @@ async function trigger_internal<TRunTypes extends AnyRunTypes>(
machine: options?.machine,
priority: options?.priority,
region: options?.region,
lockToVersion: options?.version ?? getEnvVar("TRIGGER_VERSION"),
lockToVersion: options?.version ?? scopedEnvVar("TRIGGER_VERSION"),
debounce: options?.debounce,
},
},
Expand Down Expand Up @@ -2322,7 +2329,7 @@ async function batchTrigger_internal<TRunTypes extends AnyRunTypes>(
machine: item.options?.machine,
priority: item.options?.priority,
region: item.options?.region,
lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
lockToVersion: item.options?.version ?? scopedEnvVar("TRIGGER_VERSION"),
},
};
})
Expand Down
Loading
Loading