docs(management): document TriggerClient for multi-target SDK usage (#3694)

## Summary

Docs follow-up for #3683 (`TriggerClient` for per-instance SDK
configuration). Adds a dedicated reference page and threads the new
pattern through the existing management + preview-branches docs.

## What's in

**New page** `docs/management/multiple-clients.mdx` — when to use
`TriggerClient` vs `configure()` vs `auth.withAuth`, env-var fallback
rules, isolation contract, namespace surface, `inheritContext` opt-in,
and a when-to-use-what table.

**Updated pages**

- `docs/management/authentication.mdx` — rewrote the `auth.withAuth`
section to reflect the now-ALS-backed semantics (the prior version
warned about concurrency races and pointed at issue #3298 as a tracked
fix; that fix landed in #3683). Added `tr_preview_*` to the key prefix
list. Reframed the multi-target use case to lead with `TriggerClient`,
with `auth.withAuth` as the temporary-override helper.
- `docs/management/overview.mdx` — added a `Multiple clients in one
process` subsection.
- `docs/deployment/preview-branches.mdx` — added a `Triggering across
multiple branches from one process` example.
- `docs/triggering.mdx` — one-liner pointing at the new page for
cross-project triggering.
- `docs/docs.json` — slotted `management/multiple-clients` into the
Management API nav, right after authentication.

Paired with #3683.

## Test plan

- [ ] Mintlify preview renders cleanly
- [ ] Code samples in each updated page run as documented
- [ ] Cross-page links resolve (`/management/multiple-clients`,
`/management/authentication`)
This commit is contained in:
Eric Allam
2026-06-12 17:08:04 +01:00
committed by GitHub
parent 5fab8cafcf
commit 97c12e2510
6 changed files with 149 additions and 12 deletions
+25
View File
@@ -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:
+1
View File
@@ -344,6 +344,7 @@
"pages": [
"management/overview",
"management/authentication",
"management/multiple-clients",
"management/errors-and-retries",
"management/auto-pagination",
"management/advanced-usage"
+15 -12
View File
@@ -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,15 @@ 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`).
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`).
<Warning>
Avoid `auth.withAuth` as a per-request authentication strategy on long-running servers. Use it
only for sequential, non-overlapping scopes.
</Warning>
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.
#### How scoping actually works
Unlike `TriggerClient` instances (which stay isolated unless you opt in), `auth.withAuth` keeps the surrounding task context: a call made inside a task still inherits `parentRunId`, version locking, and the test flag, the same as a direct SDK call. See the [isolation contract](/management/multiple-clients#isolation-contract).
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.
A fix using async context isolation is tracked in [issue #3298](https://github.com/triggerdotdev/trigger.dev/issues/3298).
<Note>
On runtimes without AsyncLocalStorage (browsers and some edge runtimes), the SDK falls back to
swapping the global config in place for the duration of the callback, which is not safe under
concurrency. If you need concurrent multi-target calls there, use
[`new TriggerClient({...})`](/management/multiple-clients) instances instead.
</Note>
+88
View File
@@ -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<typeof emailTask>("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.
+18
View File
@@ -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"] });
```
+2
View File
@@ -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.
<Note>
**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.
</Note>