main
7928 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
285666290f |
ci(webapp): wire the run-ops legacy guard into CI and add oxlint residency fences (#4279)
## What - Runs `apps/webapp/scripts/runOpsLegacyGuard.ts --check` as its own PR job (`runops-guard`), so code that reaches a run-graph table through the control-plane Prisma client instead of the RunStore fails the build. - Adds a `trigger-runops` oxlint plugin with two fast, in-editor rules scoped to `apps/webapp/app`: one for direct `prisma.taskRun`-style access, one for a control-plane client wired into a read-through slot. These are the cheap fence; the guard is the type-aware gate. - Fixes `CancelTaskRunService.callV1`: historical V1 runs are legacy-resident, so its two finalize writes now go through `runOpsLegacyPrisma` instead of the control-plane client (they'd miss the row once legacy is a separate database). - Regenerates the guard baseline, which had drifted stale (it referenced files deleted in an earlier PR). ## Why The guard existed but ran nowhere, so its baseline rotted and a real residency gap (the V1 cancel writes) sat undetected. Wiring it into CI turns it into a ratchet against new control-plane run-graph access. ## Verification Local, against a clean regen: `oxfmt --check`, `oxlint .`, `guard --check`, and `typecheck --filter webapp` all pass. Remaining baseline entries are 4 batch-results router reads through type-opaque `as PrismaReplicaClient` casts (correct at runtime, accepted) + 2 sanctioned legacy annotations. |
||
|
|
821972176d |
fix(run-store,webapp): correct split-database read routing, write residency, and batches list ordering (#4272)
## Summary Correctness and performance fixes for deployments that split run data across more than one database. Single-database / self-hosted deployments are unaffected (they collapse to a single read/write path). - **Batches list (dashboard):** for some organizations the Batches list could hide older batches or show them out of order. It now orders and paginates by creation time (with the id as a stable tiebreak), so every batch appears exactly once, newest first. The pagination cursor format changes; older in-flight cursors simply restart from the first page. - **Reads:** waitpoint and snapshot lookups that are keyed by a single run now read only the database that holds that run instead of querying both, removing redundant queries on hot paths (unblock, snapshot reads). - **Writes:** environment-scoped writes with no owning run (standalone wait tokens, waitpoint tags, idempotency-key resets) now land in the same database as that environment's runs, rather than defaulting to the other one. An idempotency-key reset also falls back to the other database when it matches nothing, so a reset still clears the key wherever the run actually lives. ## Notes Verified end-to-end against multi-database setups: run-keyed reads and env-scoped writes land on the correct database with no cross-database writes, and the batches list surfaces every batch in creation order. New tests cover the batches ordering/reachability and the write-residency routing. |
||
|
|
0ff0abd776 |
fix(webapp): recover from stale /build assets via a bounded reload (#4282)
## Problem The webapp's HTML references content-hashed `/build` assets, and each running instance contains exactly one build and returns 404 for asset hashes it doesn't have. During a rolling deploy a client can hold HTML from one build while a request for one of its assets is served by an instance on a different build → missing styles or a failed chunk load. ## What this does On a `/build` stylesheet/script/chunk load failure, the client does a **bounded full-document reload** (at most 2 per 5 minutes, tracked in `sessionStorage`) so the page reloads onto a single consistent build. That's the whole mechanism — no polling, no `fetch` interception, no blocking overlay, no form snapshotting. - `apps/webapp/app/components/StaleAssetRecovery.tsx` — authored as a typed, lint-checked function and serialized to an inline script via `.toString()` (so the logic is real, reviewable code, not an opaque string), injected before `<Links />`, production only. - Detection: capture-phase `error` listener for `<link>`/`<script>`/modulepreload failures under `/build/`, plus an `unhandledrejection` guard for dynamic-import failures. - Guards: once-per-page re-entrancy guard, the bounded reload budget, and a `navigator.onLine` check so it never reloads into an offline error page. - Unit tests in `StaleAssetRecovery.test.ts`. ## Relationship to #4260 Replaces the recovery introduced in #4260 (reverted in #4280) with a much smaller, reload-only approach — the previous version intercepted `fetch` and could turn a data request into a navigation, and showed a full-screen overlay on any asset error; this drops both. ## `/build-version` compatibility shim `apps/webapp/server.ts` adds a tiny `GET /build-version` endpoint (build id only, `no-store`). A previously-deployed client build polls it after an asset failure and reloads once it sees a newer build, so those older tabs recover in one reload instead of getting stuck. Temporary — safe to remove once older clients have cycled out. It deliberately does **not** re-add an `X-Build-Id` response header. ## Also Restores the `.server-changes` writing guidance in `.claude/rules/server-apps.md` (reverted alongside #4260). ## Self-hosting note Recovery is most reliable when your load balancer keeps a client on one instance for the duration of a deploy (short session stickiness) — the reload then lands on a consistent build in one hop. |
||
|
|
73d966ad22 |
chore(webapp): remove deprecated realtime stream write action (#4250)
Removes the deprecated realtime stream write action kept for retired v3 task clients. Supported clients use the targeted stream write routes, while the existing stream read loader remains unchanged. |
||
|
|
051d7080d6 |
Revert "fix(webapp): survive asset hash rotation across rolling deploys (#4260)" (#4280)
Standard `git revert` of #4260. Its client-side stale-asset recovery is net-negative during normal deploys: - The `fetch` interception treats any `?_data=` request (Remix loader **and** action traffic) as a navigation and, on a build-id mismatch, `location.assign`es the tab to the fetched URL — an open dashboard tab can be hard-navigated to a raw data URL during a rolling deploy, losing unsaved input. - Any transient `/build` asset error (a network blip, an extension, an unrelated failed dynamic import) blanks the page behind a full-screen overlay for ~60s before offering a manual reload. - It serialized form field values to `sessionStorage` to restore them across the reload. This returns the webapp to the pre-#4260 baseline as a fast, low-risk step. Follow-ups (separate PRs): - a minimal reload-only recovery to replace this, - restore the unrelated `.claude/rules/server-apps.md` docs tidy-up from #4260 (via cherry-pick), - a load-balancer stickiness change addressing the root cause. |
||
|
|
939c00782d | feat(webapp): show runtime versions in deployment lists (#4273) | ||
|
|
eccc8e3ae0 |
fix: .env.example file state DIRECT_URL without ref (#4275)
The `DIRECT_URL=${DATABASE_URL}` wasn't working in at least one user of
the var.
|
||
|
|
d7ec75d5ad |
feat(runtime): add experimental Node.js 24 and 26 task runtimes (#4085)
⚒️ Publish Worker (v4) / build (supervisor) (push) Has been cancelled
## Summary Adds experimental Node.js 24 and 26 task runtimes through the `experimental-node-24` and `experimental-node-26` config values. Existing runtime defaults and the `node`, `node-22`, and `bun` behavior remain unchanged. The unprefixed `node-24` and `node-26` config values remain unavailable until the runtimes are ready for general use. ## Design Experimental config values normalize to canonical runtime identifiers before build manifests are created, keeping deployment metadata and execution behavior consistent. Kubernetes task pods also use the runtime-default seccomp profile so modern Node.js versions fall back from io_uring to checkpoint-compatible system calls.re2-prod-supervisor-tri-11473 re2-test-supervisor-tri-11473 |
||
|
|
43250522a5 |
fix(run-store): fix batch idempotency lookup on the dedicated run-ops store (#4271)
## Summary `batchTrigger` requests that set a per-item `idempotencyKey` failed with a 500 when the run-store is split across databases: the per-item idempotency lookup errored before any run was created. Batches without per-item keys, single `trigger` idempotency, and batch-level (`idempotency-key` header) idempotency were unaffected. ## Root cause `findRunsByIdempotencyKeys` built its `UNION ALL` of per-key point-lookups with `@trigger.dev/database`'s `Prisma.sql` / `Prisma.join`, then executed it on whichever store client it was handed. On the dedicated run-ops store that client is a *separate* generated Prisma client, and a `Sql` object from a different generated client is not recognized: the bare `$queryRaw(Prisma.join(...))` form dropped the query text entirely (`Argument \`query\` is missing`). The tagged-template form is no better here: joining nested `Prisma.sql` fragments across the two clients mis-numbers the bound parameters (`syntax error at or near "$1"`). ## Fix Build the lookup as a plain parameterized string and run it via `$queryRawUnsafe` with positional placeholders and bound values, so it no longer depends on which generated client executes it. The query text contains only static SQL and integer placeholders; every value (`runtimeEnvironmentId`, `taskIdentifier`, each key) is bound, so it is not a raw-interpolation site. Same per-key point-lookup shape as before, no change on the single-client path. Verified end-to-end against a bundled build with the run-store split enabled: before the fix, `batchTrigger` with a per-item key 500s; after, it returns the runs and dedups correctly across fresh, repeat, and mixed batches. |
||
|
|
80cbc46bf6 |
fix(webapp): log transient Attio 5xx/429 at warn instead of error (#4270)
The signup → Attio sync (`attio.server.ts` `#assert`) logged every non-2xx response at `error` level and threw the same way regardless of status. Transient upstream failures (5xx/429) are retried by the common worker and self-heal, so treating them as errors created false alerts for something that isn't actually a bug. Now `#assert` splits the two cases: - **5xx / 429** — Logged at `warn` and thrown with `logLevel: "warn"`, so they continue to be retried but don't raise error-level alerts. This reuses the same pattern the worker already honors (`directorySyncEffects`). - **4xx** — Unchanged: logged at `error` and thrown, so genuine integration bugs (schema, permissions, auth, etc.) remain visible. There is no behavior change to retries or the signup flow. This is a server-only change. --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
890dd66eb5 |
feat(webapp): route ClickHouse reads to an optional read replica (#4081)
## Summary Adds optional configuration to send ClickHouse read traffic to a separate instance (for example a read replica) while writes stay on the primary `CLICKHOUSE_URL`. This lets operators offload read load (runs list, traces, logs, queries) from the cluster that handles inserts. Fully backwards compatible: with nothing new set, every client resolves to `CLICKHOUSE_URL` exactly as before. ## What it adds - `CLICKHOUSE_READER_URL` (optional): a single reader endpoint that the read-only clients fall back to. Read clients resolve `<own URL> ?? CLICKHOUSE_READER_URL ?? CLICKHOUSE_URL`. The task-events client (which both inserts events and reads traces, spans, and logs) is built as a reader/writer pair so queries use the reader while inserts stay on `CLICKHOUSE_URL`. - `RUNS_LIST_CLICKHOUSE_URL` (optional): a dedicated client for the runs list (dashboard list, runs list API, live reload, child-status counts), so the highest-traffic read path can target its own instance. ## Safety Only read-only clients fall back to the reader: logs, query, admin, runs list, the pending-version lookup, and the realtime run-id resolver. The query page is constrained to read-only (the TSQL parser rejects anything that is not a `SELECT`, and a `readonly` setting is applied). The task-events client routes inserts to the writer and queries to the reader per method, so a write can never reach the reader. Pure-write clients (event inserts, replication) always use `CLICKHOUSE_URL`. Note: this PR targets a baseline branch rather than `main` so the diff stays scoped to the read-replica changes. It will be retargeted to `main` before merge. --------- Co-authored-by: Eric Allam <eallam@icloud.com> |
||
|
|
b902e65dfb |
chore: standardise internal node on 24.18.0 (#4254)
## Summary Updates the internal development, CI, and runtime-image Node version to 24.18.0. SDK compatibility coverage continues to include Node 20, 22, 24, and 26. The Node type definitions and the package-manager lockfiles now resolve against Node 24 types. |
||
|
|
976171ea16 |
feat(webapp): management API for orgs, projects, members, and settings (#4146)
## Summary
Adds a set of PAT-authenticated management API endpoints so orgs,
projects, members/invites, environment variables, and a few
project/environment settings can be managed programmatically (scripting,
automation) rather than only through the dashboard. Each route is a thin
wrapper over the **existing** service the dashboard already uses, with
the same authorization applied at the route layer - no new business
logic.
## Endpoints
**Organizations**
- `POST /api/v1/orgs` - create an org (`createOrganization`)
- `PATCH /api/v1/orgs/:orgParam` - rename (title)
- `DELETE /api/v1/orgs/:orgParam` - soft-delete
(`DeleteOrganizationService`; keeps the active-subscription guard)
**Members & invites**
- `GET /api/v1/orgs/:orgParam/members` - list members + pending invites
- `DELETE /api/v1/orgs/:orgParam/members/:memberId` - remove a member
(last-member guarded)
- `POST /api/v1/orgs/:orgParam/invites` - invite by email
(`inviteMembers`, sends the invite email)
- `DELETE /api/v1/orgs/:orgParam/invites/:inviteId` - revoke an invite
**Projects**
- `PATCH /api/v1/projects/:projectRef` - rename
(`ProjectSettingsService`)
- `DELETE /api/v1/projects/:projectRef` - soft-delete
(`DeleteProjectService`)
- `PUT /api/v1/projects/:projectRef/default-region` - set the default
region by worker-group name (`SetDefaultRegionService`)
- project GET/list now return `defaultRegion` (worker-group name, or
null when unset)
**Environments**
- `POST /api/v1/projects/:projectRef/:env/pause` and `/resume`
(`PauseEnvironmentService`)
- `POST /api/v1/projects/:projectRef/:env/regenerate-api-key` - rotate
the env secret key (`regenerateApiKey`, RBAC `write:apiKeys`)
- env var create now accepts an optional `isSecret` flag
## Auth & authorization
- All routes authenticate with a **Personal Access Token**
(`Authorization: Bearer tr_pat_...`).
- Org/project routes are built on the PAT route builders in
`apiBuilder.server.ts`: `createLoaderPATApiRoute` (already existed) and
**`createActionPATApiRoute`** (added here - the loader builder had no
mutation counterpart). The builder runs auth, resolves the org/project
role-floor via `context`, and enforces a declarative `authorization`
block using the same RBAC actions the dashboard applies
(`manage:organization` / `read:members` / `manage:members` /
`manage:project`). Handlers keep a membership-scoped query as the floor,
so a non-member gets a 404. This also gives these routes `tenantContext`
user attribution (Sentry) and `ServiceValidationError`-to-status mapping
for free.
- **Membership floor (important).** The OSS RBAC fallback grants a
permissive ability, so `ability.can(...)` can't reject a non-member on
self-hosted. Every handler therefore resolves the target scoped to the
caller's membership (`members: { some: { userId } }`) → 404 for
non-members. `authorization` is the *role* gate; this is the *tenant*
gate. `resolveOrganizationForApiUser`
(`organizationApiAccess.server.ts`) is the org-tier version of the
existing `findProjectByRef` - org-addressed PAT routes are new, so no
such helper existed before.
- Env-tier routes reuse the existing `authorizePatEnvironmentAccess`
(`write:apiKeys`).
### What `createActionPATApiRoute` gives you
A route is pure declaration - the builder handles auth, RBAC,
validation, tracing, and error mapping:
```ts
export const action = createActionPATApiRoute(
{
method: "PUT", // one verb, or ["PATCH", "DELETE"] for multi-verb routes
params: ParamsSchema,
body: SetDefaultRegionRequestBody, // zod-validated
context: async ({ projectRef }) => { // resolve the org for the RBAC role-floor
const project = await prisma.project.findFirst({
where: { externalRef: projectRef, deletedAt: null },
select: { organizationId: true },
});
return project ? { organizationId: project.organizationId } : {};
},
authorization: { action: "manage", resource: () => ({ type: "project" }) },
},
async ({ params, body, authentication, ability }) => {
// auth + authz already enforced. Just do the work.
// `throw new ServiceValidationError("Region not found", 400)` → mapped to that status.
return json({ ok: true });
}
);
```
Handled for you, so handlers stay thin:
- **Method allowlist** - `method` accepts a verb or an array; any other
verb → `405` with an `Allow` header, *before* auth runs:
```ts
const allowedMethods = method ? (Array.isArray(method) ? method :
[method]) : undefined;
if (allowedMethods && !(allowedMethods as
string[]).includes(request.method.toUpperCase())) {
return json({ error: "Method not allowed" }, { status: 405, headers: {
Allow: allowedMethods.join(", ") } });
}
```
- **PAT / user-actor auth** in a single roundtrip → `401` on
missing/invalid/revoked token.
- **RBAC** - `context` computes the caller's role-floor for the target
org/project; `authorization` gates it → `403` with a structured error
body.
- **Sentry attribution** - `tenantContext.enrich({ userId })` so events
from the handler carry the acting user.
- **Typed errors** - a thrown `ServiceValidationError` is mapped to its
`.status` (default 400); anything else → `500`, and expected boundary
errors are logged as `warn` (kept out of Sentry).
- **Validation** - params / query / headers / body are all zod-checked →
`400` with details.
## Notes for reviewers
- Everything wraps an existing service; the intent is API parity for
things that are currently dashboard-only, not new behaviour.
- `createActionPATApiRoute` is new shared infra (the PAT + RBAC mutation
builder that didn't exist). It's self-contained - the loader builder and
existing routes are untouched.
- `@trigger.dev/core` gets one additive field (`defaultRegion` on the
project response, optional/nullable for client-server version skew) -
changeset included, patch.
- `removeTeamMember`'s last-member guard is now atomic (Serializable
transaction via the `$transaction` helper, with retry), so the dashboard
and API both get it server-side. Added a `## Transactions` rule to
`apps/webapp/CLAUDE.md` (always use the `$transaction` helper);
migrating the remaining direct usages is tracked in TRI-11698.
## Open questions
- ~~Is PAT the right auth (vs OAT for automation)?~~ **Resolved: PAT.**
Organization Access Tokens are currently internal-only (used by the
image builder) and not user-accessible, so they can't back this yet.
- Should any of these be gated behind a flag or scope?
- Naming/shape of the routes.
|
||
|
|
1ab5066ed0 |
perf(webapp,run-store): point-lookup batch idempotency keys (#4255)
## Summary Batch triggers that use per-item idempotency keys could take seconds instead of milliseconds when the target task had a large run history. This keeps the idempotency lookup fast regardless of how many runs a task has accumulated. ## Root cause The batch path checks which items already have runs by looking up their idempotency keys with a single `WHERE runtimeEnvironmentId = ? AND taskIdentifier = ? AND idempotencyKey IN (...)` query. On a very large `TaskRun` table Postgres underestimates the row count of a specific `(environment, task)` pair, so once the `IN` list grows past a handful of keys it stops doing per-key index probes and instead scans every run for that `(environment, task)` and filters the keys in memory. The cost is then flat and large regardless of how many keys are being checked, and a routine `ANALYZE` does not correct the estimate at that table size. ## Fix Look each idempotency key up on its own, batched into a `UNION ALL` of point lookups (chunked, run with bounded concurrency). Each branch is an equality on all three columns of the unique index, so the planner can only do a per-key index probe and can never fall back to the range scan. Same results, same columns, confined to the batch trigger path. |
||
|
|
2aa64200f8 |
fix(webapp): survive asset hash rotation across rolling deploys (#4260)
### Problem Webapp HTML references content-hashed /build assets, and each Docker image contains exactly one build with a hard 404 for unknown hashes. During a rolling deploy, a client holding HTML from the old build may request old asset hashes from a replica running the new image, causing missing styles or failed chunk loads. The page should recover automatically once a compatible build becomes available, without reload loops or unnecessary interruptions during normal deployments. ### What changed - Build changes alone do nothing — no polling, no automatic reloads. - If a CSS or JavaScript asset fails to load, a recovery overlay is shown immediately. - While the server still reports the same build, the client polls for a newer build using exponential backoff (up to ~60s). As soon as a newer build is detected, the page reloads automatically. - If no newer build appears within the timeout, recovery falls back to a manual Reload action. - If recovery still fails after the automatic reload, the client stops retrying and displays a final recovery screen instead of entering a reload loop. - Recovery preserves form values and scroll position across the automatic reload. |
||
|
|
c936c79e39 |
docs: update ClickHouse chat agent example for generative UI (#4251)
📚 Publish docs / publish (push) Has been cancelled
Updates the ClickHouse chat agent example page to match the upgraded example (triggerdotdev/examples#124), which is now a fullstack generative-UI chat app rather than an agent-only project. ## What changed - **Overview / tech stack / features** rewritten: Next.js chat app (`useChat` + `useTriggerChatTransport`, no API route), a `renderVisualization` tool taking json-render specs rendered with `@json-render/shadcn` + shadcn charts (Recharts) + mapcn point maps, and a shared catalog that generates both the system-prompt component reference and tool-call validation. - **The agent section** now shows the versioned [AI Prompt](https://trigger.dev/docs/ai/prompts) pattern (`prompts.define()` + `chat.prompt.set()` + `chat.toStreamTextOptions({ registry })`), with a warning that `experimental_telemetry` comes from the stored prompt — the docs previously showed a static `system:` string, which silently ships no LLM observability. - **New sections** for the shared catalog, the `renderVisualization` tool, the Next.js chat UI and registry. - **Relevant code links** updated to the new `src/` layout. - **Learn more** cards now include Frontend and AI Prompts. Note: merge after triggerdotdev/examples#124 lands, so the GitHub file links resolve. 🤖 Generated with [Claude Code](https://claude.com/claude-code)docs-release-20260714-1409 |
||
|
|
313fe03481 | test(core): fix flakey run-stream test depending on ordering (#4256) | ||
|
|
a1ca64613b |
fix(webapp): reuse the primary db pool for legacy run-ops when DSNs match (#4253)
## Summary When the run-ops split is enabled, the legacy run-ops database client was always constructed as its own connection pool, even when it points at the same database as the primary (control-plane) client. On setups where those two DSNs resolve to the same physical database, this opened a second, redundant pool and doubled the number of connections used against that database. This change makes the legacy client reuse the primary client's pool whenever their DSNs point at the same database, and only open a separate pool when they genuinely differ. ## Fix A small `sameDatabaseTarget` comparison (host, port, database name, user) decides whether the legacy DSN points at the same database as the primary. When it does, the legacy handle reuses the primary client by reference, so no second pool is opened. When the DSNs diverge, the legacy client is built independently as before, so the split still works once the databases are actually separate. Two smaller changes ride along: - An optional per-pool limit for the run-ops read replica, which connects unpooled and so draws raw backend connections; unset, it falls back to the existing default and behaviour is unchanged. - A startup warning about a missing legacy replica URL is now suppressed when the legacy client shares the primary pool, where it would be misleading. ## Verification Booted the webapp end-to-end in three modes and confirmed the pools opened as expected via the client's own startup logs and live backend connection counts: split off (single pool), split on with a shared database (legacy reuses the primary pool, no doubling), and split on with separate databases (legacy opens its own pool). |
||
|
|
165955781d |
chore: release v4.5.4 (#4228)
📚 Publish docs / publish (push) Has been cancelled
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 1s
🚀 Publish Trigger.dev Docker / scan-webapp (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-webapp (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-worker-v4 (push) Has been skipped
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🧭 Helm Chart Release / lint-and-test (push) Has been cancelled
🧭 Helm Chart Release / release (push) Has been cancelled
🚀 Publish Trigger.dev Docker / 📣 Dispatch main image (push) Has been cancelled
## Summary 2 new features, 11 improvements, 5 bug fixes. ## Breaking changes - Trigger.dev v3 is no longer supported. For self-hosted deployments, 4.5.0 is the last version we officially support for running v3; stay on 4.5.0 or upgrade to v4. v3 triggers, batch triggers, reschedules, and deploys now return a clear upgrade message instead of running. ([#4236](https://github.com/triggerdotdev/trigger.dev/pull/4236)) ## Improvements - You can now mark environment variables synced via the `syncEnvVars` build extension as secrets. Return `{ name, value, isSecret: true }` from your callback and those variables are stored redacted in the dashboard, just like manually created secret env vars. ([#4203](https://github.com/triggerdotdev/trigger.dev/pull/4203)) - Remove the legacy `--mcp` and `--mcp-port` options from the `dev` command. Run the dedicated `trigger mcp` command to start the Trigger.dev MCP server. ([#4246](https://github.com/triggerdotdev/trigger.dev/pull/4246)) - Removed the unused `ResourceMonitor` export from `@trigger.dev/core/v3/serverOnly`. It was a server-side logging helper with no remaining consumers. ([#4244](https://github.com/triggerdotdev/trigger.dev/pull/4244)) - Removed the unused `@trigger.dev/core/v3/zodNamespace` export and the legacy v3 socket message schemas. These were only used by the now-retired v3 engine and have no v4 consumers. ([#4236](https://github.com/triggerdotdev/trigger.dev/pull/4236)) ## Bug fixes - Fix a `chat.agent` message-loss race where sending a message right after an action (such as an undo) could drop the follow-up's response from the UI until a refresh. ([#4234](https://github.com/triggerdotdev/trigger.dev/pull/4234)) ## Server changes These changes affect the self-hosted Docker image and Trigger.dev Cloud: - Added `EVENT_REPOSITORY_POSTGRES_WRITES_DISABLED` to skip all PostgreSQL task-event writes for deployments that store task events in ClickHouse. Leave it off unless `EVENT_REPOSITORY_DEFAULT_STORE` is `clickhouse_v2`, otherwise task events are lost. ([#4242](https://github.com/triggerdotdev/trigger.dev/pull/4242)) - Promo credits: a /promo signup landing page, redeeming a promo code when a new org selects a plan, and showing remaining credits on the usage page. ([#4138](https://github.com/triggerdotdev/trigger.dev/pull/4138)) - Speed up retrieving a background worker by version. The endpoint no longer runs a slow lookup that scanned the full task table for large deployments; it now reuses data it already loads, so the response is the same but returns much faster. ([#4245](https://github.com/triggerdotdev/trigger.dev/pull/4245)) - Clearer login error when an email address is blocked by the WHITELISTED_EMAILS setting: the message now explains the address isn't allowed on this instance instead of the ambiguous "This email is unauthorized". ([#4220](https://github.com/triggerdotdev/trigger.dev/pull/4220)) - Make the native build server the default in project build settings. It's now opt-out, stored as a new `disableNativeBuildServer` key. Also clarifies in the UI that build settings apply to GitHub-triggered and native build server deployments. ([#3980](https://github.com/triggerdotdev/trigger.dev/pull/3980)) - Optionally process high-volume telemetry ingestion in parallel for higher throughput under heavy load by setting `OTEL_TRANSFORM_WORKER_POOL_ENABLED=1`. Off by default. ([#4232](https://github.com/triggerdotdev/trigger.dev/pull/4232)) - Add a `REALTIME_BACKEND_DEFAULT` env var to choose the default realtime backend (`electric`, `native`, or `shadow`) for environments whose org has no per-org override. Defaults to `electric`, so existing behavior is unchanged. ([#4231](https://github.com/triggerdotdev/trigger.dev/pull/4231)) - Clarified on the Regions page that a region only affects where your runs execute, not where your data is stored. This shows as a tooltip on the Location column and in the confirmation dialog when you change your default region. ([#4226](https://github.com/triggerdotdev/trigger.dev/pull/4226)) - Improved the reliability of how run data is read and written. ([#4237](https://github.com/triggerdotdev/trigger.dev/pull/4237)) - Fixed stale login errors: an error from a previous login attempt (for example a rejected email address) no longer keeps reappearing on the login page and no longer makes later, successful attempts look like they failed. ([#4220](https://github.com/triggerdotdev/trigger.dev/pull/4220)) - The Errors page now shows better details for each error. Errors that don't carry a message — such as errors thrown without a message, or values thrown that aren't `Error` objects — get a meaningful title instead of all reading "Unknown error", and are grouped by their name (or value) rather than collapsed into a single group. The error type now shows the actual error name, and stack traces now appear where previously they were missing. ([#4225](https://github.com/triggerdotdev/trigger.dev/pull/4225)) - Return a clear client error when SSO form submissions use an unsupported content type ([#4238](https://github.com/triggerdotdev/trigger.dev/pull/4238)) - Query page: extracting fields from a run's output with JSON functions (such as JSONExtractString or JSONExtractInt) no longer fails with an "illegal type: JSON" error. ([#4221](https://github.com/triggerdotdev/trigger.dev/pull/4221)) <details> <summary>Raw changeset output</summary> # Releases ## @trigger.dev/build@4.5.4 ### Patch Changes - You can now mark environment variables synced via the `syncEnvVars` build extension as secrets. Return `{ name, value, isSecret: true }` from your callback and those variables are stored redacted in the dashboard, just like manually created secret env vars. ([#4203](https://github.com/triggerdotdev/trigger.dev/pull/4203)) - Updated dependencies: - `@trigger.dev/core@4.5.4` ## trigger.dev@4.5.4 ### Patch Changes - Remove the legacy `--mcp` and `--mcp-port` options from the `dev` command. Run the dedicated `trigger mcp` command to start the Trigger.dev MCP server. ([#4246](https://github.com/triggerdotdev/trigger.dev/pull/4246)) - Updated dependencies: - `@trigger.dev/core@4.5.4` - `@trigger.dev/build@4.5.4` - `@trigger.dev/schema-to-json@4.5.4` ## @trigger.dev/core@4.5.4 ### Patch Changes - Removed the unused `ResourceMonitor` export from `@trigger.dev/core/v3/serverOnly`. It was a server-side logging helper with no remaining consumers. ([#4244](https://github.com/triggerdotdev/trigger.dev/pull/4244)) - Removed the unused `@trigger.dev/core/v3/zodNamespace` export and the legacy v3 socket message schemas. These were only used by the now-retired v3 engine and have no v4 consumers. ([#4236](https://github.com/triggerdotdev/trigger.dev/pull/4236)) ## @trigger.dev/python@4.5.4 ### Patch Changes - Updated dependencies: - `@trigger.dev/sdk@4.5.4` - `@trigger.dev/core@4.5.4` - `@trigger.dev/build@4.5.4` ## @trigger.dev/react-hooks@4.5.4 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.4` ## @trigger.dev/redis-worker@4.5.4 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.4` ## @trigger.dev/rsc@4.5.4 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.4` ## @trigger.dev/schema-to-json@4.5.4 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.4` ## @trigger.dev/sdk@4.5.4 ### Patch Changes - Fix a `chat.agent` message-loss race where sending a message right after an action (such as an undo) could drop the follow-up's response from the UI until a refresh. ([#4234](https://github.com/triggerdotdev/trigger.dev/pull/4234)) - Updated dependencies: - `@trigger.dev/core@4.5.4` </details> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>docs-release-2026-07-14 v4.5.4 helm-v4.5.4 v.docker.4.5.4 |
||
|
|
9f4d8d8b0c |
docs: update schedule & test navigation for the new dashboard UI (#4252)
The dashboard was redesigned and two pages moved, but the docs still described the old sidebar: - **Schedules** no longer has its own sidebar page — schedules are managed from the **Tasks** page (open a scheduled task to create / view / edit / enable-disable / delete them). - The standalone list-based **Test** page is deprecated — you test a task from its own **Test** button now. ## Changes - `tasks/scheduled.mdx`: rewrote the "attaching schedules" and "testing schedules" sections for the Tasks-based flow, added a "managing schedules in the dashboard" section, and added explicit callouts noting both pages moved (so readers — and search — aren't pointed at a page that no longer exists). Re-shot the four schedule screenshots and fixed a mislabeled alt text. - `run-tests.mdx`, `snippets/step-run-test.mdx`, `guides/examples/sentry-error-tracking.mdx`: replaced "select the Test page in the sidebar" with the task-first flow plus a callout, and refreshed `test-dashboard.png`. TRI-11939 |
||
|
|
64e5d732ad |
chore(webapp,core): remove the unused ResourceMonitor server logging helper (#4244)
The `ResourceMonitor` server-side logging helper is no longer used. It periodically logged the webapp process own memory, disk, and CPU usage behind the `RESOURCE_MONITOR_ENABLED` flag (off by default), and was also exported from `@trigger.dev/core/v3/serverOnly` with no other consumers. This removes the helper, its `@trigger.dev/core` export, the webapp wiring, and the `RESOURCE_MONITOR_ENABLED` env var. The supervisor has its own unrelated `ResourceMonitor` class, which is left untouched. |
||
|
|
29598a77b8 |
feat(webapp): add option to disable PostgreSQL task-event writes (#4242)
## Summary Adds `EVENT_REPOSITORY_POSTGRES_WRITES_DISABLED` (default off), which makes the task-event store skip all PostgreSQL `TaskEvent` writes. It's for deployments that store task events in ClickHouse (`EVENT_REPOSITORY_DEFAULT_STORE=clickhouse_v2`) and no longer want the PostgreSQL copy. ## How it works The guard sits at the single postgres write boundary, `TaskEventStore.create` / `createMany`, so it covers every write path (OTLP ingestion and run-lifecycle events) with one check. Reads are untouched (`findMany` / trace queries / streaming), so existing PostgreSQL events remain readable. Leave it off unless the default store is `clickhouse_v2`, otherwise task events for any run still routed to PostgreSQL would be dropped. |
||
|
|
6e943f2421 | chore(cli): remove --mcp option from trigger dev (#4246) | ||
|
|
e0b42a88d6 |
perf(webapp): avoid unindexed fileId scan in get-background-worker-by-version (#4245)
## What
The `GET
/api/v1/projects/:projectRef/background-workers/:envSlug/:version`
endpoint loaded each file's tasks through the nested `files.tasks`
relation. Prisma resolves that as a separate query:
```sql
SELECT id, slug, "fileId" FROM "BackgroundWorkerTask" WHERE "fileId" IN (...)
```
`BackgroundWorkerTask.fileId` is not indexed — the FK constraint exists,
but Postgres does not auto-create an index for foreign keys — so on a
large table this can only run as a sequential scan, which gets
progressively slower as the table grows and was observed taking minutes
per call in production.
The loader already loads every task for the worker via `tasks: true`,
which uses the indexed `workerId` relation, and those rows already
include `fileId`. This PR groups task slugs by `fileId` in memory from
that already-loaded data and drops the `files.tasks` include entirely.
## Behavior change (latent bug fix)
The response shape is unchanged, but there is a semantic correction for
**source files reused across worker versions** (files are de-duplicated
by `@@unique([projectId, contentHash])`, so one file row can be linked
to many workers).
- **Before:** `file.tasks` came from the `BackgroundWorkerFile.tasks`
relation, i.e. *every* `BackgroundWorkerTask` with that `fileId` —
across all workers sharing the file. So a worker's manifest could list
tasks it doesn't actually have.
- **After:** `file.tasks` is grouped from the queried worker's own
tasks, so it reflects only that worker version's tasks.
Verified on a local DB: 460 files are referenced by tasks from more than
one worker; of 6819 (worker, file) pairs, 6 differ — all one file where
the old union leaked a task slug (`cancellation-test`) into worker
versions that never had it. The new per-worker behavior is the correct
one for a worker-version manifest. (Thanks to the automated review for
flagging this.)
## Analysis
Captured the exact SQL before/after by instrumenting Prisma against real
data (a worker with 62 files):
- **Before:** 5 statements, including the `WHERE "fileId" IN (...)`
scan.
- **After:** 4 statements; the `fileId` query is gone and the other four
are identical.
EXPLAIN of the two access paths:
```
Before WHERE "fileId" IN (...)
Seq Scan on "BackgroundWorkerTask"
Filter: ("fileId" = ANY (...)) -- reads the whole table, scales with table size
After WHERE "workerId" IN (...)
Index Scan using "BackgroundWorkerTask_workerId_slug_key"
Index Cond: ("workerId" = ...) -- bounded by matching rows, scale-independent
```
No new index is required: the `workerId` access path is already covered
by the existing `BackgroundWorkerTask_workerId_slug_key` unique index.
## Testing
- `pnpm run typecheck --filter webapp` passes.
- Query capture + EXPLAIN performed against a local database seeded with
real worker/file/task data.
|
||
|
|
703a6dcb4c |
chore(ci): optimise runners, distribute test shards (#4240)
- Use bigger/smaller runners as recommended by warpbuild - Distribute test shards more evenly, move internal tests single big shard |
||
|
|
022e5c1ad0 |
chore(deps): pin transitive deps and upgrade nodemailer to 9 (#4243)
Routine dependency maintenance. - Pin a few high-fanout transitive deps to current patched versions via `pnpm.overrides`: `form-data`, `ws`, `undici`, `hono`. Lockfile-only (no published-package dependency changes); net shrinks via dedup. - Upgrade `nodemailer` 8 → 9 in `internal-packages/emails` (private package). The SES transport already uses SESv2 and the `createTransport`/`sendMail` API is unchanged, so no code changes were needed. `@types/nodemailer` stays at 8 (no 9.x published yet; types are compatible). Verified locally: `pnpm i` clean; `pnpm run typecheck --filter emails` and `--filter webapp` both pass. |
||
|
|
bea7e2be90 |
feat(webapp,run-store): route run-graph reads and writes through the run-store router (#4237)
## Summary Run-graph data (runs, batches, waitpoints, and their related tables) can now live in a database separate from the control plane, with every read and write routed to the correct database by each run's residency. This makes reading and writing run data more reliable once the two are split, and is a no-op for single-database installs. ## Design - Run-graph table access goes through the run-store router, which selects the legacy or the new run-ops store per run instead of assuming one shared client. - The legacy run-ops client is now independently pointable, so legacy run data can be served from its own database (and replica) rather than the control-plane connection. - Run-graph writes go straight to the run-graph database instead of being forwarded through the control plane, and replication targets are split so runs in the new database still replicate to analytics without under-counting. - Read-through slots refuse the control-plane client, so a missing residency fails loudly instead of silently reading the wrong database. - Migration `20260710120000_drop_remaining_run_graph_seam_foreign_keys` drops the foreign keys that still crossed the run-graph / control-plane seam, which is what lets the two live in separate databases. The split stays off unless explicitly enabled and the two databases are confirmed physically distinct; startup fails closed otherwise. Verified by running the full dashboard end-to-end suite against both a single-database configuration and a three-database configuration (control plane, the new database, and a physically separate legacy database), with runs on both residencies. No misrouted reads in either configuration. |
||
|
|
c23585710c |
docs: note v3 is retired and 4.5.0 is the last version supporting v3 (#4241)
## Summary Refreshes the docs for the v3 sunset: v3 (SDK v3) is end of life, and 4.5.0 is the last version we officially support for running v3. - The self-hosting overview, plus the docker and kubernetes version-locking sections, now tell self-hosters on v3 to stay on 4.5.0 or migrate to v4. 4.5.1 and later reject v3 triggers and deploys with an upgrade message. - The migration guide's deprecation notice was still written in the future tense (with dates that have since passed); it now describes v3 as retired and adds the self-hosted 4.5.0 cutoff. This is the page the server's upgrade message links to. - Fixes a stale "v3 project" reference in the CLI overview. The Mintlify preview will render the callouts for a visual check. |
||
|
|
5ba8557a51 |
chore(webapp,core): remove the end-of-life v3 (engine V1) execution stack (#4236)
## Summary v3 (the engine that ran the SDK v3 era, internally `RunEngineVersion.V1`) is end-of-life. Following the removal of the v3 execution apps ([#4194](https://github.com/triggerdotdev/trigger.dev/pull/4194)) and the legacy dev websocket ([#4198](https://github.com/triggerdotdev/trigger.dev/pull/4198)), this removes the remaining v3 execution stack from the server. Clients still on v3 (an old SDK or CLI that has not upgraded) keep getting a clear "upgrade to v4" response. Triggers, batch triggers, reschedules, and deploys that resolve to v3 are rejected with a graceful 4xx pointing at the migration guide, never a 5xx, so a stale client cannot affect server health. Self-hosted instances still running v3 should stay on the 4.5.x release line until they migrate. ## What is removed - The MarQS queue and its shared/dev queue consumers. - The v3 socket.io namespaces (coordinator, provider, shared-queue) and the v3 run lifecycle services (attempt, checkpoint, and batch-resume). - The graphile-worker background job system; all live jobs already run on `@trigger.dev/redis-worker`. - The `DEPRECATE_V3_ENABLED` flag: v3 is now rejected unconditionally, so the flag is gone. - Unused v3 exports from `@trigger.dev/core` (the `v3/zodNamespace` subpath and the legacy socket message catalogs) and the now-dead MarQS environment variables. ## What stays The v4 engine is untouched. The graceful v3 rejection boundary stays, `determineEngineVersion` still detects a v3 project so it can reject it, and the batch service plus batch-completion worker stay for current clients. Live queue concurrency limits and metrics now read from the v4 run engine instead of MarQS, and a brand-new dev environment now defaults to v4. ## Dependency cleanup Removes webapp dependencies left unused by this change: `seedrandom` and `semver` (only the removed v3 code used them) plus a set that was already dead, their orphaned `@types` packages, and two dead files. Adds a `knip:deps` script and a `knip.json` config so unused dependencies can be found the same way going forward. |
||
|
|
c0f7c803b1 |
fix(webapp): return 415 for invalid SSO form content types (#4238)
## Summary SSO form submissions with an unsupported content type now receive a 415 response instead of failing while parsing the request body. |
||
|
|
c601739d35 |
perf(webapp,run-store): grouped run-ops reads + mint-kind flip grace (#4227)
## Summary Two threads on the run-ops split path. Read path: per-item run reads are batched into grouped queries, a waitpoint's connected-run reads are bounded, and the dedicated-schema relation hydrators fetch only the requested columns instead of whole rows. Retrieve also falls back to the other database when a routed read misses, so a run whose physical residency diverges from its id shape is still found rather than returning a spurious not-found. Fewer and lighter queries on the run read path, with no change to results. Mint-kind flip safety: flipping which database new runs mint to is now a deterministic wall-clock cutover, for both per-org and global flips. For a grace window every process resolves the same database, so a flip cannot route two concurrent triggers that share an idempotency key to different databases (which would bypass the per-database unique constraint and create a duplicate run). Supersedes the earlier #4205 and #4208. Draft: validation in progress. |
||
|
|
5f2541d94f |
feat(webapp): make native build server the default in build settings (#3980)
Switches the native build server from opt-in to opt-out in project build settings. - It's now enabled by default, stored as a new \`disableNativeBuildServer\` opt-out key so previously-saved \`useNativeBuildServer: false\` values aren't treated as deliberate opt-outs. - The "Use native build server" checkbox is checked by default; unchecking it persists the opt-out. - Brief wording: clarifies build settings apply to GitHub-triggered and native build server deployments, and the native build server hint no longer says "in the future". |
||
|
|
fda8e77175 |
fix(docs): openapi labels for different bulk api variants (#4223)
Replace Option 1 Option 2 etc with labelled variants. |
||
|
|
45527e317a |
feat(webapp): opt-in worker pool for OTLP ingest transform (#4232)
## Summary Under high OTLP ingest volume, the whole decode, transform, and enrich pipeline runs on the request event loop, so a single CPU core becomes the ceiling while the rest sit idle. This adds an opt-in worker pool that moves decode, transform, and LLM-cost enrichment onto worker threads, keeping the main thread free for I/O. It is off by default (`OTEL_TRANSFORM_WORKER_POOL_ENABLED`), so behavior is unchanged unless enabled. ## Design Workers do decode, filter, convert, and enrich (including LLM pricing match). The main thread stays the single database reader: it loads the pricing registry and broadcasts the compiled model rows to the workers (re-broadcasting on every reload), so workers never touch the database. The pure transform is extracted into a dependency-light module (no Prisma/Redis/ClickHouse imports) so it can run inside a worker. Importantly, the main thread keeps the existing single consolidated insert path, so ClickHouse insert batching and part count are unchanged. The parallelism buys CPU headroom, not more insert streams (which would add merge pressure). The worker is bundled as a standalone file at build time and ships in the existing image with no Dockerfile change. In local load testing the pool sustained roughly 2.6x the throughput of the single-thread path and kept the main thread responsive under load. |
||
|
|
9b3a7bd7b2 |
fix(sdk,webapp): stop chat losing a message sent right after an action (#4234)
## Summary Sending a chat message immediately after an action (for example an undo) could make the message's response vanish from the UI. The transport opened a response stream that closed on the *earlier* turn's completion instead of waiting for the send's own turn. The agent still produced and persisted the answer, so it reappeared on refresh. Same "disappearing message" class as [#4176](https://github.com/triggerdotdev/trigger.dev/pull/4176), different cause. ## Fix A send's response stream had no way to tell whether a `turn-complete` belonged to its turn. `POST /realtime/v1/sessions/:id/in/append` now returns the appended record's sequence number, and the transport skips any turn-complete whose `session-in-event-id` (the agent's committed `.in` cursor) is below that seq, closing only on its own turn. Older webapps omit the seq, in which case the transport falls back to the previous behavior, so the SDK and server can ship independently. Because the fix spans the SDK and the server, both a webapp deploy and an SDK release are needed for the full effect. Verified end to end with the ai-chat reference app: undo-then-immediate-send loses the follow-up's answer before the fix and streams it inline after, with a revert-the-guard run reproducing the loss on the same script. Unit tests cover the skip and the no-seq fallback. |
||
|
|
5d0e9d9dc5 |
feat(webapp): make the default realtime backend configurable (#4231)
## Summary The default realtime backend was hardcoded to Electric. This adds a `REALTIME_BACKEND_DEFAULT` env var (`electric` | `native` | `shadow`, default `electric`) that chooses the backend for any environment whose org has no `realtimeBackend` override. Behavior is unchanged unless you set it; per-org overrides still win. The default is applied at every point where the per-org flag falls through: the initial value, the flag lookup default, and the error fallback. |
||
|
|
2cac63f13a |
fix: improve error labelling, grouping, and stack traces in the Errors feature (#4225)
## Problem Several display/grouping issues in the **Errors** feature, all rooted in how the ClickHouse error materialized views (`errors_mv_v1`, `error_occurrences_mv_v1`) read the stored error JSON produced by `parseError`: 1. **Messageless errors show "Unknown error".** An empty message falls straight through `coalesce(nullIf(message,''), 'Unknown error')` to the literal, even though the error's class `name` is available (e.g. an Effect tagged error `ListMessagesError` with no message). 2. **Unrelated errors collapse into one group.** `calculateErrorFingerprint` keys on `type : message : stack`, where `type` is always the union tag (`BUILT_IN_ERROR`, …), `message` is empty, and the stack isn't read — so every messageless built-in error (and every string/custom error) hashes to the same constant input → one fingerprint. 3. **error_type shows the internal tag.** `coalesce(type, name, …)` always resolves to `type` (always present), so the column shows `BUILT_IN_ERROR` instead of the real class name. 4. **Stack traces never populate.** The MVs read `error.data.stack`, but the serializer stores the trace under `stackTrace` — so the column is always empty. ## Fix All display changes are `ALTER TABLE … MODIFY QUERY` on the two views (migration `035`); the fingerprint change is in the webapp. - **Fingerprint** (`errorFingerprinting.ts`): fall back **message → name → raw**. Messageless errors now group by class name (or raw value for non-Error throws); message-bearing errors are **unchanged** (short-circuits at `message`), so existing groups don't split — only currently-messageless errors get their own group going forward. - **error_message**: same `message → name → raw` fallback before `'Unknown error'`. - **error_type**: coalesce `name → code → 'Error'` (drops the reliance on the union tag). Built-in → class name, internal → `code`, string/custom → `Error`. - **stack trace**: read `error.data.stackTrace`. Bounded as before (serializer caps 50 frames / 1024 chars per line; MV clips to 2000 chars). ## Migration notes - `MODIFY QUERY` swaps the view query in place (no drop/recreate gap); Down restores the previous query. - **Existing rows are left unchanged** — changes apply only to rows inserted after the migration. No backfill. ## Tests `errorFingerprinting.test.ts` — 57 pass, incl. new cases for messageless class names, string/custom raw values, and stability of message-bearing fingerprints. Fixes the display-derivation half of TRI-11938 (error_type + stack trace); relates to TRI-9254 and TRI-9250. |
||
|
|
4be32d411c |
fix(webapp): keep the last Owner on directory-sync role changes (#4230)
Applying a directory-sync effect that would demote the org's last Owner (a group remap, or a provision) previously threw and 500'd the settings save. Now rbac.setUserRole reports code:"last_owner" and applyEffect skips just that member (they keep Owner) while the rest of the batch applies. Adds the machine-readable RoleAssignmentResult.code to the plugin contract so callers can tell the last-owner guard apart from a real failure. |
||
|
|
b64b54c74e |
feat(webapp): pass database writer and reader config to auth plugins (#4229)
## Summary The RBAC and SSO auth plugins can own their own database client, but they could only read `DATABASE_URL`, so every connection they opened landed on the primary. The host webapp now resolves writer and read-replica URLs from its env (the same fallback chain its own Prisma clients use: control-plane URL first, then the default) and passes them to the plugins at create time via a shared `PluginDatabaseConfig`, along with separate connection limits for writes (default 2) and reads (default 5, tunable via `RBAC_DATABASE_*_CONNECTION_LIMIT` and `SSO_DATABASE_*_CONNECTION_LIMIT`). A plugin can then route hot-path reads (per-request auth checks, login routing) to the read replica and keep only rare mutations on the primary. With no replica configured, or no plugin installed, nothing changes: the OSS fallback ignores the new option and keeps reading through the Prisma clients it is already given. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
25eb0c71a0 |
fix(webapp): clarify that region only affects where runs execute (#4226)
## Summary This adds an always-visible info tooltip on the Location column and a note in the "set default region" confirmation dialog making it explicit. It also removes the obsolete "V4" badge from the Regions page title. |
||
|
|
48a0b83ec6 |
feat(webapp): promo credits — /promo signup landing, redeem at plan selection, usage display (#4138)
## What & why Signup promo credits. A new logged-out `/promo?code=<code>` landing page validates the code and carries it through signup via a cookie. When the new organization is activated by selecting a plan, the code is redeemed and its credits are applied; the usage page then shows the remaining promo credits and their expiry. ## Notes - The code is redeemed at **plan selection**, not org creation: the credit grant targets the org's usage allowance, which only exists once a plan is selected — applying at creation would have nothing to grant onto. Redemption is best-effort and never blocks plan selection. - Pairs with the corresponding billing-service change (promo code validate/apply/credits + grant issuance); the two are released together. ## Testing Verified locally end to end: `/promo` shows the offer, a new account carries the code through signup, selecting the Free plan redeems it, and the usage page shows the remaining credits. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
983bd03131 |
feat: support isSecret in syncEnvVars (#4203)
## What
Adds per-variable secret support to the `syncEnvVars` build extension.
Return `{ name, value, isSecret: true }` and the variable is stored as a
secret (redacted in the dashboard, value non-revealable), just like a
manually created secret env var. Secret and non-secret variables can be
mixed in one callback.
```ts
syncEnvVars(async () => [
{ name: "PUBLIC_API_URL", value: "https://api.example.com" },
{ name: "DATABASE_URL", value: "postgres://...", isSecret: true },
]);
```
## How
Env vars flow through the build pipeline as a flat name→value map, and
the import API's `isSecret` is per-call. So secret vars are carried
through the layer + manifest in parallel `secretEnv` / `secretParentEnv`
maps, and at deploy time they go up in a second `importEnvVars` call
with `isSecret: true` (the plain vars in the first call). The record
form (`{ KEY: "value" }`) is unchanged and stays non-secret.
## Commits
- `feat(core)`: carry secret env vars through the build layer + manifest
schema
- `feat(build)`: partition `isSecret` vars in `syncEnvVars`
- `feat(cli)`: merge secret layers and import them with `isSecret: true`
at deploy
- `test(build)`: cover the partitioning + document `isSecret`
## Testing
- vitest covers the partitioning (secret/non-secret × child/parent) and
that the record form stays non-secret.
- Verified against a local webapp that the deploy's import contract
stores the secret var redacted (`isSecret: true`) and the plain var
visible.
Closes TRI-11099
|
||
|
|
7faa52597d |
chore: format prisma schemas (#4224)
Creating a Prisma migration now formats its schema first, keeping migration-related schema edits consistently formatted without adding work to the repository-wide format command. Run `pnpm run format:prisma` to format either schema on demand. |
||
|
|
02cf9c81ad |
fix(tsql): make JSON functions work on the output and error columns (#4221)
## Summary A Query page (TRQL) query that pulls fields out of a run's `output` with JSON functions (`JSONExtractString`, `JSONExtractInt`, `JSONHas`, and the rest of the family) failed with "The first argument of function ... should be a string containing JSON, illegal type: JSON". Those queries now work. ## Root cause and fix `output` is a native ClickHouse `JSON` column, but `JSONExtract*`, `JSONHas`, `JSONLength`, and `JSONType` all expect a String containing JSON text. The compiler already swaps in the column's String companion (`output_text`) when a JSON column is selected or compared, but not inside function-call arguments, so it emitted `JSONExtractInt(output, 'x')` against the native column. The fix prints the companion column for the first argument of these functions when it resolves to a bare JSON field, keeping the table alias when qualified (so it works in JOINs): JSONExtractInt(output, 'x') -> JSONExtractInt(output_text, 'x') JSONExtractArrayRaw(assumeNotNull(output), 'y') -> JSONExtractArrayRaw(assumeNotNull(output_text), 'y') It also reaches through value-preserving passthrough wrappers like `assumeNotNull(...)`, while leaving value-changing wrappers like `toJSONString(output)` on the native column (that argument is already a String). The swap is also semantically correct, not just a type fix: `output_text` is the unwrapped data JSON that the TRQL `output` model already represents, so field paths line up. Covered by printer unit tests and a ClickHouse integration test that runs the whole family (plus the wrapped and `toJSONString` cases) against a real native-JSON column. Both new cases fail with the exact "illegal type: JSON" error without the fix. |
||
|
|
de536622c8 |
Add oxlint rule to catch thrown un-awaited redirect helpers (#4222)
## ✅ Checklist - [x] I have followed every step in the [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md) - [x] The PR title follows the convention. - [x] I ran and tested the code works --- ## Testing - Verified the rule emits exactly five errors for un-awaited throws of the known async redirect helpers while ignoring awaited throws, returned promises, and synchronous `redirect(...)`. - Verified `--fix` inserts `await` in async functions and produces a clean second lint run. - Verified synchronous functions remain diagnostic-only so autofix cannot introduce invalid syntax. - Ran `pnpm run format`, `pnpm run lint`, `pnpm run typecheck --filter webapp`, and `git diff --check`. --- ## Changelog Adds an Oxlint rule that prevents async redirect helpers from being thrown without awaiting their `Response`. Existing violations are fixed, the autofix is limited to async functions, and the plugin uses an explicit ESM extension. --- ## Screenshots See the test-results comment for CLI evidence. 💯 Link to Devin session: https://app.devin.ai/sessions/e60ad7610773401da3d3040cf1252337 Requested by: @ericallam --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Eric Allam <eric@trigger.dev> |
||
|
|
b4866f0184 |
docs: improve bulk actions docs (#4211)
- Combine SDK and dashboard bulk actions docs - Fix API reference pages for bulk actions - Fix weird rendering on bulk actions page ## Todo - [ ] not sure about having the SDK+dashboard combined and under "Using the dashboard"... need to find the right place |
||
|
|
e6e8aeb993 |
docs(limits): document automatic payload offloading for triggers and batches (#4217)
## Summary The limits page didn't spell out that large payloads offload to object storage automatically, and its single "512KB" note conflated two different thresholds. This clarifies the behaviour. On the way in, the SDK uploads any trigger or batch-item payload over 128KB to object storage before sending, so large triggers and batches don't hit the request body limit (`trigger` / `triggerAndWait` since 4.5.0, `batchTrigger` / `batchTriggerAndWait` since 4.5.2). On retrieval, payloads and outputs over 512KB are stored in object storage and returned as a presigned URL from `runs.retrieve`. |
||
|
|
32e5edbd04 |
fix(webapp): restore magic link login on the login page (#4220)
## Summary Magic link login could appear completely broken: submitting your email on the login page showed a stale "This email is unauthorized" error instead of the "we've sent you a magic link" confirmation, even when the address was fine. This PR reverts [#4215](https://github.com/triggerdotdev/trigger.dev/pull/4215) (whose diagnosis and fix turned out to be wrong) and fixes the actual bug, which was in how login errors are stored and consumed. ## Root cause Two session bugs compounded on the login page: - The `/login` loader read the flashed `auth:error` without committing the session. A Remix flash is only consumed when the session is committed after the read, so once any attempt flashed an error (for example an address rejected on an instance with `WHITELISTED_EMAILS` set), it stayed in the session cookie and reappeared on every later `/login` visit, making successful attempts look like failures. - The `/login/magic` action stored its validation and rate limit errors with `session.set`, which survives every later read and commit, so those errors stuck permanently. [#4215](https://github.com/triggerdotdev/trigger.dev/pull/4215) had instead diagnosed a server-only module leaking into the client bundle and crashing navigation. Checking the shipped images' client bundles via their sourcemaps shows `.server` modules were always stubbed out, so that change fixed nothing and is reverted here. ## Fix - `/login` reads the flashed error and commits the session when one was present, so an error renders once and clears. The `redirectTo` branch now surfaces the error too instead of leaving it in the cookie. - `/login/magic` flashes its errors instead of `set`ting them. Verified end-to-end on a live preview environment: a rejected address shows the error once and a reload clears it; a valid address lands on the confirmation screen with the address named; GitHub, Google, and SSO login paths are untouched by this diff. |
||
|
|
9f76c92021 |
chore: release v4.5.3 (#4219)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 3s
🚀 Publish Trigger.dev Docker / scan-webapp (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-webapp (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-worker-v4 (push) Has been skipped
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🧭 Helm Chart Release / lint-and-test (push) Has been cancelled
🧭 Helm Chart Release / release (push) Has been cancelled
🚀 Publish Trigger.dev Docker / 📣 Dispatch main image (push) Has been cancelled
## Summary 1 improvement, 2 bug fixes. ## Breaking changes - Removed support for the end-of-life v3 `trigger dev` CLI. Starting a dev session with an old v3 CLI now returns an upgrade message instead of connecting - upgrade to the v4 CLI to continue using `trigger dev`. ([#4198](https://github.com/triggerdotdev/trigger.dev/pull/4198)) ## Bug fixes - Fix TS2742 ("inferred type cannot be named") when exporting a `chat.agent` from a project with declaration emit: `ChatTaskWirePayload` and `ChatInputChunk` are now declared in the public `@trigger.dev/sdk/chat` subpath, so inferred agent types emit portable declarations and the wire types are directly importable. ([#4218](https://github.com/triggerdotdev/trigger.dev/pull/4218)) ## Server changes These changes affect the self-hosted Docker image and Trigger.dev Cloud: - Reduce primary database load on the runs page by serving its empty-state check from ClickHouse instead of Postgres. ([#4202](https://github.com/triggerdotdev/trigger.dev/pull/4202)) - Fixed submitting your email on the login page reloading back to an empty form instead of showing the magic link confirmation screen. ([#4215](https://github.com/triggerdotdev/trigger.dev/pull/4215)) <details> <summary>Raw changeset output</summary> # Releases ## @trigger.dev/build@4.5.3 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.3` ## trigger.dev@4.5.3 ### Patch Changes - Updated dependencies: - `@trigger.dev/build@4.5.3` - `@trigger.dev/core@4.5.3` - `@trigger.dev/schema-to-json@4.5.3` ## @trigger.dev/python@4.5.3 ### Patch Changes - Updated dependencies: - `@trigger.dev/sdk@4.5.3` - `@trigger.dev/build@4.5.3` - `@trigger.dev/core@4.5.3` ## @trigger.dev/react-hooks@4.5.3 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.3` ## @trigger.dev/redis-worker@4.5.3 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.3` ## @trigger.dev/rsc@4.5.3 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.3` ## @trigger.dev/schema-to-json@4.5.3 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.3` ## @trigger.dev/sdk@4.5.3 ### Patch Changes - Fix TS2742 ("inferred type cannot be named") when exporting a `chat.agent` from a project with declaration emit: `ChatTaskWirePayload` and `ChatInputChunk` are now declared in the public `@trigger.dev/sdk/chat` subpath, so inferred agent types emit portable declarations and the wire types are directly importable. ([#4218](https://github.com/triggerdotdev/trigger.dev/pull/4218)) - Updated dependencies: - `@trigger.dev/core@4.5.3` ## @trigger.dev/core@4.5.3 </details> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>helm-v4.5.3 v.docker.4.5.3 v4.5.3 |
||
|
|
25254d0201 |
fix(sdk): make inferred chat agent types portable for declaration emit (#4218)
## Summary
Exporting a `chat.agent` from a project with `declaration: true` failed
with TS2742: the inferred type of the agent references
`ChatTaskWirePayload`, which was declared in an internal module not
reachable through the package exports map, so tsc could only name it via
a file path into `node_modules` and refused to emit. Consumers had to
hand-mirror the wire type and annotate their export.
## Fix
`ChatTaskWirePayload` and `ChatInputChunk` are now declared in
`@trigger.dev/sdk/chat` (a public subpath) and re-exported type-only
from the internal shared module, so every internal import is unchanged
and the browser/server module split is untouched. Declaration emit for
an inferred agent type now produces a portable specifier:
```ts
export declare const chatAgent: Task<"chat-agent", import("@trigger.dev/sdk/chat").ChatTaskWirePayload<MyUIMessage, MyClientData>, unknown>;
```
As a side effect the wire types are now directly importable, which is
what affected users were reconstructing by hand.
## Verification
Reproduced against the built 4.5.2-equivalent package: a consumer
fixture with declaration emit produced `import("<file
path>/ai-shared.js")` in its declaration (the TS2742 trigger); after the
fix the same fixture emits the public specifier with zero diagnostics. A
regression test now builds that consumer simulation in a temp directory
on every test run: it copies the built package into a fake node_modules
(copied, not symlinked, because tsc only applies exports-map naming to
real node_modules paths), compiles the fixture with the TypeScript API,
and asserts no errors, no relative-path imports, and no internal module
references in the emit.
|