From 5b5e9af117364bd30be8ef2e142ce577cfea94f6 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 21 May 2026 17:01:10 +0100 Subject: [PATCH 1/2] docs(management): document TriggerClient for multi-target SDK usage Adds a dedicated `Multiple SDK clients` page covering when to reach for `new TriggerClient({...})` vs `configure()` vs `auth.withAuth`, the env-var fallback rules, the isolation contract, and the curated namespace surface. Updates the existing management overview, authentication, preview branches, and triggering pages to surface the new pattern. Refreshes the `auth.withAuth` section: removes the stale concurrency warning and the reference to issue #3298, since the fix landed alongside TriggerClient. Adds the `tr_preview_*` key prefix to the example. --- docs/deployment/preview-branches.mdx | 24 ++++++++ docs/docs.json | 1 + docs/management/authentication.mdx | 22 +++---- docs/management/multiple-clients.mdx | 85 ++++++++++++++++++++++++++++ docs/management/overview.mdx | 17 ++++++ docs/triggering.mdx | 2 + 6 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 docs/management/multiple-clients.mdx diff --git a/docs/deployment/preview-branches.mdx b/docs/deployment/preview-branches.mdx index f2a354e2e9d..1a1d5fbd20c 100644 --- a/docs/deployment/preview-branches.mdx +++ b/docs/deployment/preview-branches.mdx @@ -66,6 +66,30 @@ 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", +}); + +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..73a181186a3 --- /dev/null +++ b/docs/management/multiple-clients.mdx @@ -0,0 +1,85 @@ +--- +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", +}); + +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 +const client = new TriggerClient(); + +await client.tasks.trigger("send-email", { to: "..." }); +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..0cd2d44f2b2 100644 --- a/docs/management/overview.mdx +++ b/docs/management/overview.mdx @@ -44,4 +44,21 @@ 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", +}); + +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. From 4d1c5920ef59b45949e2ba2ac770238d8858646c Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 21 May 2026 22:52:49 +0100 Subject: [PATCH 2/2] docs(management): make TriggerClient snippets self-contained Define `payload` and surface the `emailTask` type import so the example snippets across overview, multiple-clients, and preview branches docs run as pasted. --- docs/deployment/preview-branches.mdx | 1 + docs/management/multiple-clients.mdx | 5 ++++- docs/management/overview.mdx | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/deployment/preview-branches.mdx b/docs/deployment/preview-branches.mdx index 1a1d5fbd20c..d2fea48c885 100644 --- a/docs/deployment/preview-branches.mdx +++ b/docs/deployment/preview-branches.mdx @@ -82,6 +82,7 @@ const checkout = new TriggerClient({ previewBranch: "checkout-redesign", }); +const payload = { to: "user@example.com" }; await Promise.all([ signupFlow.tasks.trigger("send-email", payload), checkout.tasks.trigger("send-email", payload), diff --git a/docs/management/multiple-clients.mdx b/docs/management/multiple-clients.mdx index 73a181186a3..d94ed593a27 100644 --- a/docs/management/multiple-clients.mdx +++ b/docs/management/multiple-clients.mdx @@ -15,6 +15,7 @@ const preview = new TriggerClient({ previewBranch: "signup-flow", }); +const payload = { to: "user@example.com" }; await prod.tasks.trigger("send-email", payload); await preview.runs.list({ status: ["COMPLETED"] }); ``` @@ -50,9 +51,11 @@ If no `accessToken` resolves from either the constructor or env vars, the first 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: "..." }); +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: "..." }); diff --git a/docs/management/overview.mdx b/docs/management/overview.mdx index 0cd2d44f2b2..4f3316a32bd 100644 --- a/docs/management/overview.mdx +++ b/docs/management/overview.mdx @@ -59,6 +59,7 @@ const preview = new TriggerClient({ 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