diff --git a/docs/deployment/preview-branches.mdx b/docs/deployment/preview-branches.mdx
index f2a354e2e9d..d2fea48c885 100644
--- a/docs/deployment/preview-branches.mdx
+++ b/docs/deployment/preview-branches.mdx
@@ -66,6 +66,31 @@ async function triggerTask() {
}
```
+### Triggering across multiple branches from one process
+
+If a single process needs to trigger runs in several preview branches (or a mix of prod and preview), use `new TriggerClient({...})` for each target instead of mutating global config. Each instance owns its own auth and branch.
+
+```ts
+import { TriggerClient } from "@trigger.dev/sdk";
+
+const signupFlow = new TriggerClient({
+ accessToken: process.env.TRIGGER_PREVIEW_KEY,
+ previewBranch: "signup-flow",
+});
+const checkout = new TriggerClient({
+ accessToken: process.env.TRIGGER_PREVIEW_KEY,
+ previewBranch: "checkout-redesign",
+});
+
+const payload = { to: "user@example.com" };
+await Promise.all([
+ signupFlow.tasks.trigger("send-email", payload),
+ checkout.tasks.trigger("send-email", payload),
+]);
+```
+
+See [Multiple SDK clients](/management/multiple-clients) for the full pattern.
+
## Preview branches with GitHub Actions (recommended)
This GitHub Action will:
diff --git a/docs/docs.json b/docs/docs.json
index ee670bca2da..fd8eecb5900 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -342,6 +342,7 @@
"pages": [
"management/overview",
"management/authentication",
+ "management/multiple-clients",
"management/errors-and-retries",
"management/auto-pagination",
"management/advanced-usage"
diff --git a/docs/management/authentication.mdx b/docs/management/authentication.mdx
index 1a32f3bc01c..3c205c18d84 100644
--- a/docs/management/authentication.mdx
+++ b/docs/management/authentication.mdx
@@ -19,7 +19,7 @@ import { configure, runs } from "@trigger.dev/sdk";
// Using secretKey authentication
configure({
- secretKey: process.env["TRIGGER_SECRET_KEY"], // starts with tr_dev_ or tr_prod_
+ secretKey: process.env["TRIGGER_SECRET_KEY"], // starts with tr_dev_, tr_prod_, or tr_preview_
});
function secretKeyExample() {
@@ -159,9 +159,12 @@ await envvars.update("proj_1234", "preview", "DATABASE_URL", {
});
```
-### Scoped authentication with `auth.withAuth`
+### Talking to multiple projects, environments, or branches
-`auth.withAuth` runs a callback with a temporary API client configuration, then restores the previous configuration when the callback resolves or rejects. It's useful when a single process needs to make calls across multiple Trigger.dev projects or environments without mutating the global config manually.
+A long-running process often needs to talk to more than one Trigger.dev target. There are two patterns:
+
+- **`new TriggerClient({...})`** — an explicit instance that owns its own auth, baseURL, and preview branch. Use this when the targets are long-lived (a dashboard that watches prod + preview, a worker that triggers across multiple projects, etc.). Each instance is fully isolated and concurrent calls don't interfere. See [Multiple SDK clients](/management/multiple-clients) for details.
+- **`auth.withAuth(config, fn)`** — runs a single callback under a temporary config override, then restores. Use this for short, sequential overrides (e.g. one batch under a different token) where keeping a dedicated client around is overkill.
```ts
import { auth, runs } from "@trigger.dev/sdk";
@@ -174,15 +177,6 @@ const projectBRuns = await auth.withAuth(
);
```
-Any SDK call inside the callback uses the overridden token. Calls outside the callback continue to use whatever was set by `configure` (or picked up from `TRIGGER_SECRET_KEY`).
-
-
- Avoid `auth.withAuth` as a per-request authentication strategy on long-running servers. Use it
- only for sequential, non-overlapping scopes.
-
-
-#### How scoping actually works
-
-Despite looking block-scoped, `auth.withAuth` stores the overridden configuration in a process-wide global (not [AsyncLocalStorage](https://nodejs.org/api/async_context.html)). It saves the previous config, installs the new one globally, runs the callback, and restores the previous config in a `finally`. This means sequential, non-overlapping usage is safe, but concurrent usage is not — if two `auth.withAuth` calls overlap (for example inside `Promise.all` with different tokens, or across concurrent request handlers on a long-running server) both will share whichever configuration was installed most recently, and SDK calls in one scope can silently use the other scope's token.
+Any SDK call inside the callback uses the overridden config. Calls outside the callback continue to use whatever was set by `configure` (or picked up from `TRIGGER_SECRET_KEY`).
-A fix using async context isolation is tracked in [issue #3298](https://github.com/triggerdotdev/trigger.dev/issues/3298).
+The override is scoped via [AsyncLocalStorage](https://nodejs.org/api/async_context.html), so concurrent `auth.withAuth` calls (including overlapping calls inside `Promise.all` with different tokens) do not interfere. Nested calls compose — an inner `auth.withAuth({ accessToken })` inside an outer `auth.withAuth({ baseURL })` runs with both fields applied.
diff --git a/docs/management/multiple-clients.mdx b/docs/management/multiple-clients.mdx
new file mode 100644
index 00000000000..d94ed593a27
--- /dev/null
+++ b/docs/management/multiple-clients.mdx
@@ -0,0 +1,88 @@
+---
+title: Multiple SDK clients
+sidebarTitle: Multiple SDK clients
+description: Use TriggerClient to talk to multiple Trigger.dev projects, environments, or preview branches from a single process.
+---
+
+The global `configure()` API binds the SDK to one set of credentials per process. When a single process needs to talk to more than one Trigger.dev project, environment, or preview branch, use `new TriggerClient({...})` for each target instead. Each instance owns its own auth, baseURL, and preview branch, and concurrent calls across instances stay isolated.
+
+```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",
+});
+
+const payload = { to: "user@example.com" };
+await prod.tasks.trigger("send-email", payload);
+await preview.runs.list({ status: ["COMPLETED"] });
+```
+
+## Configuration
+
+`TriggerClient` accepts the same fields as `configure()`:
+
+| Field | Description | Env-var fallback |
+| --------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
+| `accessToken` | Secret key (`tr_dev_*`, `tr_prod_*`, `tr_preview_*`) or personal access token (`tr_pat_*`). | `TRIGGER_SECRET_KEY`, then `TRIGGER_ACCESS_TOKEN` |
+| `previewBranch` | Preview branch name when using a `tr_preview_*` key. | `TRIGGER_PREVIEW_BRANCH`, then `VERCEL_GIT_COMMIT_REF` |
+| `baseURL` | Override the Trigger.dev API URL. Defaults to `https://api.trigger.dev`. | `TRIGGER_API_URL` |
+| `requestOptions`| Request-level options (retry policy, additional headers, etc.) — see the `ApiRequestOptions` type. | — |
+
+Fields not passed to the constructor fall back to the matching env var (and then to a sensible default for `baseURL`). Explicit constructor values always win, so you can mix env-var-backed clients and fully explicit clients in the same process.
+
+```ts
+// Picks up TRIGGER_SECRET_KEY / TRIGGER_PREVIEW_BRANCH from env.
+const fromEnv = new TriggerClient();
+
+// Explicit values override env entirely.
+const explicit = new TriggerClient({
+ accessToken: process.env.OTHER_PROJECT_KEY,
+ previewBranch: "feature-x",
+});
+```
+
+If no `accessToken` resolves from either the constructor or env vars, the first API call throws an `ApiClientMissingError` with a clear message.
+
+## What's on a TriggerClient instance
+
+Each instance exposes the management surface as namespaced properties: `tasks`, `runs`, `batch`, `schedules`, `envvars`, `queues`, `deployments`, `prompts`, and `auth`.
+
+```ts
+import type { emailTask } from "./trigger/email";
+
+const client = new TriggerClient();
+
+await client.tasks.trigger("send-email", { to: "user@example.com" });
+await client.runs.list({ status: ["COMPLETED"], limit: 10 });
+await client.schedules.create({ task: "daily-report", cron: "0 9 * * *" });
+await client.envvars.update("proj_1234", "preview", "DATABASE_URL", { value: "..." });
+```
+
+Methods that only make sense inside a running task are not on the instance surface: `tasks.triggerAndWait`, `tasks.batchTriggerAndWait`, `tasks.triggerAndSubscribe`, `batch.triggerAndWait`, `batch.triggerByTaskAndWait`, and the task-definition helpers (`schedules.task`, `prompts.define`).
+
+## Isolation contract
+
+When you make a call through a `TriggerClient` instance, the SDK does not look at the process-wide global config, env vars (other than the constructor-time fallback), or the ambient task context. Two instances pointing at different projects can run in the same process — including in parallel under `Promise.all` — without interfering with each other.
+
+That isolation also means a call from inside a task does not automatically inherit the surrounding task's `parentRunId`, `lockToVersion`, or test flag. If you specifically want a call to inherit those (rare — usually you want a clean external trigger), opt in with `inheritContext: true`:
+
+```ts
+const sameProject = new TriggerClient({
+ accessToken: process.env.TRIGGER_SECRET_KEY,
+ inheritContext: true,
+});
+```
+
+## When to use what
+
+| Scenario | Recommended |
+| ------------------------------------------------------------------------- | ------------------------------------ |
+| Single process, single project/env | `configure()` (or env vars only) |
+| Single process talking to multiple projects, envs, or branches | `new TriggerClient({...})` per target |
+| Short, sequential override (e.g. one batch under a different token) | `auth.withAuth(config, fn)` |
+| Inside a task, trigger a run in a different project | `new TriggerClient({...})` |
+
+See [Authentication](/management/authentication) for the underlying token types and the `auth.withAuth` helper.
diff --git a/docs/management/overview.mdx b/docs/management/overview.mdx
index 0a7ca5a44a9..4f3316a32bd 100644
--- a/docs/management/overview.mdx
+++ b/docs/management/overview.mdx
@@ -44,4 +44,22 @@ async function main() {
}
main().catch(console.error);
+```
+
+### Multiple clients in one process
+
+If a single process needs to talk to more than one Trigger.dev project, environment, or preview branch, use `new TriggerClient({...})` for each target instead of `configure()`. Each instance owns its own auth and config, with no shared global state. See [Multiple SDK clients](/management/multiple-clients) for the full pattern.
+
+```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",
+});
+
+const payload = { to: "user@example.com" };
+await prod.tasks.trigger("send-email", payload);
+await preview.runs.list({ status: ["COMPLETED"] });
```
\ No newline at end of file
diff --git a/docs/triggering.mdx b/docs/triggering.mdx
index 62ab9d5cb61..cb2608269a9 100644
--- a/docs/triggering.mdx
+++ b/docs/triggering.mdx
@@ -29,6 +29,8 @@ Trigger tasks **from inside a another task**:
When you trigger a task from your backend code, you need to set the `TRIGGER_SECRET_KEY` environment variable. If you're [using a preview branch](/deployment/preview-branches), you also need to set the `TRIGGER_PREVIEW_BRANCH` environment variable. You can find the value on the API keys page in the Trigger.dev dashboard. [More info on API keys](/apikeys).
+If a single process needs to trigger across multiple projects, environments, or preview branches, use [`new TriggerClient({...})`](/management/multiple-clients) for each target instead of relying on the global env vars.
+
**Which trigger pattern should I use?** If your triggering code can import the task definition (same codebase), use `yourTask.trigger()` for full type safety. Use `tasks.trigger()` with a type-only import when the task runs in a separate service or you need to avoid bundling task code into your app (common in Next.js). Both do the same thing at runtime.