719 Commits

Author SHA1 Message Date
Oskar Otwinowski b98cceb8fb docs: task version skew protection, --external-id, and the atomic deployments deprecation (#4742)
New deployment/version-skew-protection page: the skew problem, the
--external-id primitive and its reuse behaviour, runtime discovery (call
option, configure(), TRIGGER_EXTERNAL_DEPLOYMENT_ID, and the gated
platform/CI/generic commit-SHA variables with the build-time caveat),
the
manual any-platform recipe, waiting/expiry semantics, precedence, and
automatic skew protection on Vercel. Deprecation callouts on the atomic
deployments page and the Vercel integration page; --external-id/--force
added to the CLI deploy reference; redirect from
deployment/vercel-skew-protection so existing webapp links resolve
(TRI-13002).
2026-08-21 18:09:16 +02:00
Chris Arderne 74db5a3f58 docs: document cron schedule windows (#4657) 2026-08-18 13:23:29 +01:00
Saadi Myftija 7e677008ed feat(supervisor): per-org placement overrides for run pods (#4655)
The supervisor now supports routing an organization's runs to specific
nodes. `KUBERNETES_ORG_PLACEMENT_OVERRIDES` takes JSON keyed by the
internal org ID, adding node selector entries and tolerations to that
org's run pods, e.g. to route an org onto a dedicated, tainted node
pool:

```json
{"<orgId>": {"nodeSelector": {"pool": "dedicated"}, "tolerations": "dedicated=runs:NoSchedule"}}
```

The node selector merges over the defaults (the override wins on key
collision, with a warning logged). Tolerations append to the existing
runner and scheduled-run sets. Overrides are validated at startup
similar to `KUBERNETES_RUNNER_TOLERATIONS`.

Exposed in the Helm chart as
`supervisor.config.kubernetes.orgPlacementOverrides`, where tolerations
can also be given as a list.
2026-08-18 12:16:23 +00:00
Chris Arderne 7d9f1a3268 docs: document additional environment API keys (#4406) 2026-08-17 17:31:50 +01:00
Katia Bulatova 480bede0ad feat(webapp,sdk): dashboard agent plan enforcement, component gallery — and fixes (#4516)
Plan enforcement for the dashboard agent — message quota and watch
limits — plus the component gallery, fixes and test hardening from the
same stack (#4548, #4549, #4550, #4552, #4556 merged here).

## Plan enforcement
([TRI-12863](https://linear.app/triggerdotdev/issue/TRI-12863))

**Agent message quota.** The Free-plan allowance becomes a real
server-side limit with a durable counter. New `agent_message_usage`
table keyed `(organization_id, period)` — deliberately not joined to
chats, so deleting a chat can't free quota within the period. Both send
paths count one user message (wakes never count) and refuse at the cap
with `403 message_quota_reached`, which the client renders as an upgrade
panel, never a silent drop. The refusal code is a single shared constant
on both sides.

**Watch limits.** A watch whose window exceeds the plan's
`agentWatchMaxHours`, or that would push the org past its
`agentWatchers` count, is refused with `watch_limit_reached` (409 on the
API, an upgrade hint on the card). Plan limits only tighten the existing
code ceilings (`min(plan, 24h)`, per-chat cap of 3 still applies). A
plan limit of zero means zero, not unlimited. Questions answerable
instantly are answered before any plan refusal — a one-shot consumes no
slot and never sees an upgrade nag.

**Fails open by design.** Cloud ships the actual per-plan numbers
separately (TRI-12863 P0). Until then absent limits resolve to the
unlimited sentinel and the upgrade UI is gated on billing presence —
self-hosted sees no cap, no upsell, with tests proving the fallback.
Both quotas are nudges, not security boundaries: a failing limit read
never blocks a send.

## Component gallery

An admin-only gallery of every agent card state: five
`storybook.agent-*` pages (chat UI, view blocks, report, investigation,
watch) with their shared shell and manifest, demo fixtures, two
demo-only cards, toast examples, and the screenshot script. No LLM and
no data — every state renders from fixtures under
`dashboard-agent/demo/`, never reachable from a production path.
Designers and reviewers can look at every state, including the report
states, without seeding anything.

## And fixes

**SDK: watch-mode chat subscriptions survive quiet windows** (TRI-13065,
TRI-13070) — watch mode keeps reconnecting across empty long-poll
windows and only stops on abort or a settled session; a passive
subscriber can no longer stop a turn it doesn't own (`stopOnAbort` is
explicit, default off). Review findings fixed alongside: a superseded
stream's async teardown no longer removes the live successor's abort
controller or multi-tab claim, and stopping a generation hands the chat
back to the user's other tabs.

**Query boundary pinned end-to-end**
([TRI-11165](https://linear.app/triggerdotdev/issue/TRI-11165)) — a
route-level test drives `api.v1.query` with a real signed environment
JWT (writes refused before ClickHouse, a read passes); `readonly=1` made
non-overridable; a per-turn cap stops the model burning a turn rewriting
a query it can't fix (deterministic SQL errors only — busy/transport
rejections don't count).

**chat.agent durability regression suite**
([TRI-11166](https://linear.app/triggerdotdev/issue/TRI-11166)) —
testcontainers-backed coverage of the two audit criticals (cross-tenant
isolation, no duplicate mid-stream turn, both control-broken) plus
crash-resume, cursor-based refresh, clean rollback of a mid-write turn
failure (torn by a real constraint violation), and OOM-restart replay.

**Investigation sweep backoff** — stale investigations get an attempt
counter and backoff so a poison row can't pin the sweep queue head
(migration `0005`: `sweep_attempts`, `last_sweep_attempt_at`).

## Screenshots

<img width="1440" height="791" alt="Screenshot 2026-08-06 at 00 36 19"
src="https://github.com/user-attachments/assets/6a68cd42-8580-469d-afe7-e28d1eef18e1"
/>

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-12 13:59:35 +02:00
Katia Bulatova 4569657923 feat(webapp): dashboard agent — chat, reports, investigate (#4418)
## What & why

This is the system behind the Dashboard Agent — an assistant that
answers questions about a project's runs, errors, queues, deploys and
health, and can investigate failures end to end.

The agent runs as a chat.agent task in its own Trigger project. It has
no access to the main database or ClickHouse; all platform data is read
through the public API using a delegated, read-only user token.

Everything here is behind `canAccessDashboardAgent` and inert with the
flag off. The UI that mounts the panel lands in #4529.

## Stack

`#4418` (this, base) ← `#4529` UI ← `#4525` Watch ← `#4516` storybook
gallery. The scenario/contract reference for the whole stack is
`internal-packages/dashboard-agent/GUIDEBOOK.md` (it lands on the Watch
branch): it states, per feature, what makes each thing happen and where
that is decided.

## What's inside

**Agent runtime and tools** — `internal-packages/dashboard-agent`:
prompt, tool set (API reads, TRQL query, docs, navigation,
evidence/investigations, repo source), conversation compaction, a
prompt-prefix token budget pinned by snapshot test, and sampled
LLM-judged turn evals. The package cannot import webapp server code,
which is what makes the "no DB access" claim structural rather than a
convention.

**Contracts** — `internal-packages/dashboard-agent-contracts`:
`trigger://` URIs, intents, and the block envelope every rendered card
travels in.

**Conversation store** — `internal-packages/dashboard-agent-db`: drizzle
over postgres-js in its own `trigger_dashboard_agent` Postgres schema,
plus one additive migration.

**Auth boundary** — the user-actor token gains an optional environment
claim; one guard (`userActorEnvironment.server.ts`) enforces it so
routes don't each re-derive the rule. Token minting, cap ceiling, and
the RBAC fallback path for self-hosted.

**Transport** — webapp resource routes that mint the token and proxy
each turn, and SDK-side mid-turn reconnect.

**Public API the agent reads through** — orgs, projects, environments,
runs, queue metrics, workers, a run's commit metadata, repo snapshot,
reports, and `POST /api/v1/query`.

**Reports** — the health report's layout is declared once and shared by
the card, the markdown surface and the JSON/MCP surface, so the same
report reads the same in the dashboard, the terminal and an editor.

**Block renderers** — the report and investigation cards the flows above
already emit (`app/components/dashboard-agent/`). The panel that hosts
them, and the rest of the chat UI, is #4529.

**Query safety and CSP** — see below.

## Key decisions

- **The agent is a separate Trigger project, not webapp code.** It reads
platform data over the public API with a delegated user-actor token
whose `cap` ceilings it to read scopes. No Prisma, no ClickHouse, no
webapp imports.
- **The PAT-only auth helper now refuses user-actor tokens.** This is an
intentional behavioral change: its callers consume only a bare userId
and do not enforce delegated-token capabilities. Actor-aware routes
continue through the scoped route builders instead.
- **RBAC fallback builds a delegated token's ability from its own cap**,
never the blanket ability a PAT gets (read-only when the token declares
none). Without this, the agent's read-only cap would buy a write JWT on
self-hosted.
- **Org creation checks RBAC only for user-actor tokens, and only after
the env gate**, so an install with `ORG_CREATION_API_ENABLED` off
returns 404 rather than 403, and an ordinary PAT never consults an
ability the route has no org to scope. Both orderings are pinned by
test.
- **The query path is read-only in depth.** TRQL rejects write
statements at the grammar level (they don't parse, rather than being
filtered), ClickHouse runs with `readonly=1`, and the org/project/env
filters are injected server-side from the credential — the request body
cannot widen scope. An unparseable query denies instead of falling
through to the permissive resource.
- **Document-wide img-src CSP.** Remote images are an
outbound-request/exfiltration surface, so the policy permits only
own-origin/data/blob, the required SSO avatar hosts, and the favicon
endpoint. Operators can add exact origins through CSP_IMG_SRC_ALLOWLIST;
wildcard hosts and bare schemes are intentionally not allowed.
- **The chat transport reconnects on a mid-turn EOF**
(`@trigger.dev/sdk`). A body that ends without a turn-complete is
terminal only when the server says `X-Session-Settled: true`; otherwise
the transport resubscribes from `lastEventId` with bounded backoff, and
any record re-earns the budget. Previously a closed long-poll window or
a proxy restart left the reply stuck as if still generating.
- **Conversations live in their own datastore**, schema-scoped and
foreign-key-free (it references `organizationId`/`userId` by id, because
in cloud it is a different database). It is a display read-model for the
History tab and transport resume; `chat.agent`'s object-store snapshot
remains the model's source of truth.
- **Deterministic first.** Reports and health checks contain no LLM —
they are computed from the same data the dashboard shows, and the model
only narrates and links them. That is what makes a number in an answer
auditable.

## Testing

- 63 new test files, run with `pnpm run test --filter webapp` and
per-package vitest. Heaviest coverage on the auth boundary
(`userActorPatOnlyBoundary`, `userActorTokenClaimsAndScopes`,
`contextlessPatRoutes`, `rbacFallbackBranch`), TRQL read-only, the
report layout, and the SDK reconnect.
- The agent package has a separate eval lane (`pnpm run test:evals`,
`vitest.eval.config.ts`) that hits the real model, so it never runs in
`pnpm test`.
- Live-tested against a local stack scenario by scenario; the GUIDEBOOK
lists the condition each behaviour is expected under, which is what
those runs were checked against.

## Changelog

`.server-changes/dashboard-agent.md`, plus changesets for
`@trigger.dev/core` (report schemas), `@trigger.dev/sdk` (chat
reconnect) and the CLI's `mint-token` help text.
2026-08-11 18:56:14 +02:00
Saadi Myftija 02de2e693f feat(api): separate rate limit budget for deployment endpoints (#4565)
Most deploy-flow API calls shared the general per-environment rate limit
bucket with all of that environment's runtime traffic, so an org with
heavy API usage could intermittently 429 its own deploys; the
`/api/v*/deployments` endpoints themselves were fully exempt from rate
limits as a stopgap
([#2774](https://github.com/triggerdotdev/trigger.dev/pull/2774)), which
promised a dedicated limiter as the follow-up. This is that follow-up:
the whole deploy-flow group now runs on its own budget, separate from
runtime API limits.

### Design

A new `deploymentRateLimiter` covers every endpoint the deploy flow
depends on: the `/api/v*/deployments` group, the env API key exchange
(`/api/v1/projects/:ref/:env`), build-time env var resolution and sync
(`/envvars`, `/envvars/:slug/import`), preview branches,
`/api/v1/remote-build-provider-status` and `/api/v1/artifacts`. The
general API limiter whitelists the same shared path list, so exactly one
limiter applies to each path and the two can't drift apart.

Buckets are keyed per environment for environment API keys and per token
for the PAT-authenticated phase of a CLI deploy (whoami, key exchange,
branches). The deploy budget is controlled via the
`DEPLOYMENT_RATE_LIMIT_*` env vars.
2026-08-11 17:51:31 +02:00
DKP 336f515001 docs(ai): add LLM observability page (#4568)
## Summary

Adds a docs page for LLM observability: every opted-in Vercel AI SDK
call inside a task becomes its own span in the run trace, carrying the
model, provider, token counts, cost, and latency. The page covers
turning it on per call with `experimental_telemetry: { isEnabled: true
}`, what each span inspector tab shows (Overview, Messages, Tools, and a
Prompt tab when linked), linking a call to its prompt version with
`toAISDKTelemetry()`, and querying usage across runs with TRQL against
the `llm_metrics` table.

It sits in the AI dropdown under Features, next to
[Prompts](https://trigger.dev/docs/ai/prompts), and cross-links the
[Query](https://trigger.dev/docs/observability/query) page.

It is explicit that capture is opt-in per call (not automatic) and only
covers Vercel AI SDK calls, and notes the `@ai-sdk/otel` requirement on
AI SDK 7. Every API name, span tab, and TRQL column was checked against
the SDK and the live query schema.

## Also in this PR

Corrects one bullet in the [AI Agents
overview](https://trigger.dev/docs/ai-chat/overview): it claimed an
in-progress chat resumes on the new version after a redeploy, which
contradicts the version-upgrades and backend pages. Chat agent runs are
pinned to the version they started on; moving onto new code is an
explicit version upgrade.
2026-08-11 14:40:31 +00:00
DKP e367899510 docs(ai-agents): add chat.agent guide and refresh the AI agent guides (#4524)
## Summary

Adds a "Build a chat agent" guide to the AI agents section, surfaces the
ClickHouse chat agent example in the guides index and the AI agents
overview,
and refreshes the five existing workflow guides so their code is
current.

## Details

The pattern guides (prompt chaining, routing, parallelization,
orchestrator,
evaluator-optimizer) still used retired models and dated APIs. Updated
them to
current Anthropic Claude models (claude-haiku-4-5 for lightweight
classifier
roles, claude-sonnet-4-5 for the main work) and modernized the code:

- route-question uses generateObject for the routing decision instead of
  generateText plus manual JSON parsing.
- verify-news-article uses ModelMessage in place of the renamed
CoreMessage.
- Fixed translate-and-refine discarding its recursive refinement result,
so
  refined translations never returned to the caller.
- Fixed an invalid JSON test payload in generate-translate-copy.

The pattern concepts are unchanged; only the example code was stale.
2026-08-11 14:32:32 +00:00
Matt Aitken 04f9c4e1a5 fix(webapp,run-engine,core): drop the hidden debounce ceiling, fail fast on an unusable maxDelay (#4521)
Debouncing with a `delay` longer than an hour did nothing at all.

The engine applied a server-side ceiling on how long a debounced run
could be pushed back, measured from the run's `createdAt` and defaulting
to one hour. A run is only pushed back while its new execution time
stays inside that ceiling, so a `delay` at or above it could never push
anything: the waiting run was released, the trigger started its own run,
and the next trigger repeated it. A `delay: "12h"` produced one run per
trigger, each correctly delayed by 12h, with no error raised and nothing
on the run to show the debounce key had been ignored.

The ceiling is now unset by default. A debounce key with no `maxDelay`
keeps collapsing triggers for as long as they keep arriving, which is
what the docs have always described. Self-hosters who want a bound can
still set `RUN_ENGINE_MAXIMUM_DEBOUNCE_DURATION_MS`.

That has a consequence worth stating plainly, so the docs now carry a
warning for it: with no `maxDelay`, a continuously triggered key never
executes. Set `maxDelay` when the work has to happen eventually.

**Failing fast on an unusable `maxDelay`.** A caller who sets `maxDelay`
no longer than their `delay` hits exactly the dead end described above,
so that pair is now rejected at trigger time instead of silently
behaving as if no debounce were set:

```
debounce.maxDelay (1h) must be longer than debounce.delay (12h). A debounced run is only
pushed back while it stays inside maxDelay, so with these values every trigger would create
its own run.
```

An unparseable `maxDelay` is rejected too, rather than quietly falling
back to no bound at all, and so is a `delay` given as a date rather than
a duration, which could never work because the value is re-applied on
every push.

The same check runs against a configured server ceiling, so a
self-hosted deployment that sets
`RUN_ENGINE_MAXIMUM_DEBOUNCE_DURATION_MS` gets the error rather than the
silent failure this PR is about. With no `maxDelay` and no configured
ceiling, which is the default, there is nothing to conflict with and
nothing is rejected.

The docs, the `TriggerOptions` JSDoc and the engine option all now state
that the room available to push is the gap between `delay` and
`maxDelay`. The run engine suite gains the case that motivated this:
four triggers on one key with a 12h delay now collapse to a single run.
2026-08-07 07:55:35 +00:00
Matt Aitken f8e1c910f7 docs(ai-chat): guide for migrating an AI SDK route handler to chat.agent (#4519)
## Summary

Adds a docs page for developers who already have a working Vercel AI SDK
chat app (`useChat` on the client, an `app/api/chat/route.ts` calling
`streamText`) and want to move it to `chat.agent`. There was no page
covering that path. `ai-chat/upgrade-guide` reads like it should be the
one, but it covers moving prerelease `chat.agent` code to the Sessions
release, which is a different reader.

The page is structured around what stays, what goes, and what is new,
because the reassuring part of this migration is how much is untouched:
the `streamText` call, model config, tool definitions, `useChat`, and
all message rendering carry over as-is. What gets deleted is the route
handler, the persistence glue wired into it, and any resumable-stream
setup. What is new is the agent task, two server actions, and
`useTriggerChatTransport`.

Covers moving tools onto the agent config so `toModelOutput` survives
past turn one, where existing database persistence goes
(`hydrateMessages` plus the turn hooks), a short section on what
durability you get once you are across, a note that
Hono/SvelteKit/Express follow the same shape, and a gotchas list built
from the mistakes this specific migration produces.

## Head Start

The one thing this migration makes worse is the opening response of a
new chat. The route handler answered out of a warm process; the agent
run has to be dequeued and booted first. That is the complaint the page
has to answer head on, so Head Start gets a full section rather than a
closing aside, plus a callout up top next to the "what changes" table so
nobody plans the migration without knowing it exists.

The section walks the four steps: splitting tool schemas away from tool
executes (the bundle-isolation constraint the whole feature rests on),
building the handler, mounting it back at `app/api/chat/route.ts` with
the original auth check wrapped around it, and the transport option.
Both server actions stay, because Head Start only owns the first turn.
Three gotchas go with it: a slow first turn without Head Start, Head
Start on but the route bundle still heavy, and the route timing out
because the handler holds the SSE response open for the whole turn
rather than just step 1.

The coding-agent prompt names Head Start as explicitly out of scope, so
an agent handed the migration does not attempt the tool split
unprompted.

Also fixes the `chat.headStart` example on `ai-chat/fast-starts`, which
set `stopWhen: stepCountIs(15)` after the spread.
`toStreamTextOptions()` pins `stopWhen` to `stepCountIs(1)`, so
overriding it makes the warm handler run steps the agent is supposed to
own (and `stepCountIs` was never imported in that snippet either).

## Migration prompt

The page also ships a copy-pasteable prompt for handing the migration to
a coding agent. It tells the agent to run `npx trigger.dev@latest
skills` first, so it picks up guidance version-pinned to the SDK
actually installed in the project, then read `quick-start.md`,
`frontend.md`, and `reference.md` (with `llms.txt` as the index) before
editing anything. The instructions are explicit about preserving the
existing model, prompt, and tool schemas rather than rewriting them.

Registered in `docs.json` under Agents, directly after Quick Start, so
it is picked up by the generated `llms.txt` and the per-page `.md`
variants.
2026-08-06 17:37:23 +02:00
Eric Allam 58bf4e2833 feat(webapp): per-client database pool and connect timeout overrides (#4515)
## Summary

Follow-on to #4513. The database connect timeout is now honored, but a
single global value has to serve three separate databases at once
(control-plane, legacy run-ops, and run-ops). This adds optional
per-client overrides for the Prisma pool and connect timeouts, one pair
for the writer and one for the read replica of each of the three
databases, each falling back to the shared `DATABASE_POOL_TIMEOUT` /
`DATABASE_CONNECTION_TIMEOUT` when unset.

That lets one database's clients run a fail-fast connect timeout (with a
bounded pool wait) while another keeps more headroom, without a single
knob forcing the same tradeoff everywhere. No behavior change until an
override is set.

It also tags each client's queries with its specific datasource
(`control-plane` / `legacy-run-ops` / `run-ops`, writer or replica) via
the `db.datasource` span attribute, so telemetry can attribute
connection behavior to a specific database instead of just
writer-vs-replica.
2026-08-05 17:26:41 +01:00
Chris Arderne 1a16d61a37 fix(build): support decorator metadata with TypeScript 7 (#4505)
## Summary

Allow projects using TypeScript 7 to enable `emitDecoratorMetadata()`
without adding the TypeScript 6 compiler to every Trigger.dev CLI
installation. Addresses #4500.

## Fix

The extension now resolves TypeScript from the project and
feature-detects the legacy compiler API. TypeScript 5 and 6 continue
using the project's compiler, while TypeScript 7 projects can install
Microsoft's optional `@typescript/typescript6` compatibility package
alongside TypeScript 7.

When no compatible compiler API is available, the build reports an
actionable installation error. The extension documentation includes
setup commands for npm, pnpm, and Bun.

Verified with TypeScript 5, TypeScript 6, TypeScript 7 with and without
the compatibility package, emitted decorator metadata, packed ESM and
CommonJS consumers, package export checks, and typechecking.
2026-08-05 16:33:32 +01:00
nicktrn 8f9db53350 feat(supervisor): configurable tolerations for run pods (#4491)
## Summary

Self-hosted Kubernetes deployments can now add tolerations to run pods,
so runs
can schedule onto tainted nodes. Previously the only way to do this was
to patch
the supervisor.

`KUBERNETES_RUNNER_TOLERATIONS` takes a comma separated list of
`key=value:effect`, or `key:effect` to tolerate any value. It applies to
every
run pod, and for runs from a schedule tree it merges with the existing
`KUBERNETES_SCHEDULED_RUN_TOLERATIONS`. Left unset, nothing changes: no
tolerations are added and the pod spec leaves the field off entirely.

The Helm chart takes it as a list:

```yaml
supervisor:
  config:
    kubernetes:
      runnerTolerations:
        - dedicated=runs:NoSchedule
        - spot:NoExecute
```

## Naming

The issue proposed `KUBERNETES_WORKER_TOLERATIONS`. This ships as
`KUBERNETES_RUNNER_TOLERATIONS` instead, because `RUNNER_*` is already
the prefix
for run pod settings (`RUNNER_HEARTBEAT_INTERVAL_SECONDS`,
`RUNNER_ADDITIONAL_ENV_VARS`, and `DOCKER_RUNNER_NETWORKS` for the
Docker
equivalent), whereas "worker" refers to the supervisor itself throughout
this app.

## Validation

Keys and values are checked against the Kubernetes naming rules when the
supervisor starts, so `dedicated=prod runs:NoSchedule` fails immediately
with a
message naming the offending entry. Without that check a bad value is
accepted at
startup and then rejected by the API server on every pod create, which
stops all
runs with the cause buried in an API error.
`KUBERNETES_WORKER_NODETYPE_LABEL` is
trimmed and validated for the same reason: surrounding whitespace is not
valid in
a label value, so a padded value fails every pod create today.

## Node selector off switch

`KUBERNETES_WORKER_NODETYPE_LABEL` accepts an empty string to skip the
node
selector entirely, so runs schedule on any node. This already worked and
the Helm
chart has always shipped it empty, but it was not documented. It is now.

The issue also asked for general node affinity configuration. That is
not
included: the node selector off switch plus tolerations covers the
reported
problem, and a free form affinity setting is a much larger config
surface to
commit to.

Fixes #4458
2026-08-03 15:40:41 +00:00
nicktrn d9f4fea939 docs: restructure self-hosting kubernetes guide (#4481)
Restructures the Kubernetes self-hosting guide around two explicit paths
- an **evaluation install** (bundled datastores, one command) and a
**production install** (external datastores, your own secrets) - so
every configuration decision belongs to one path or the other instead of
being a flat list of options with caveats.

Also in this pass:

- Adds an architecture overview (component-to-`values.yaml` map) and a
post-install "verify it" step.
- Consolidates the previously scattered upgrade notes into a single
collapsible group, and cuts implementation detail and historical asides
that no longer apply.
- Removes a duplicated object-storage section (two configs in two
styles) and trims the Docker ClickHouse note down to what a self-hoster
needs to act on.
2026-08-03 11:57:24 +01:00
Matt Aitken cb9aefd49b fix(hosting): deploy ClickHouse from the official image instead of Bitnami (#4249)
## Summary

Self-hosted deployments now run ClickHouse from the official
[`clickhouse/clickhouse-server`](https://hub.docker.com/r/clickhouse/clickhouse-server)
image instead of `bitnamilegacy/clickhouse`. Bitnami's free image
catalog is EOL and the frozen legacy archive tops out at ClickHouse
25.7.5, below the 25.8 minimum the platform requires since v4.5.0, which
broke every ClickHouse insert on chart-bundled deployments. Both stacks
now default to 26.2, the same version the platform is developed and
tested against.

Existing deployments keep their ClickHouse data with no manual
migration.

Fixes #4197.

## Details

**Docker Compose**: the `clickhouse` service uses the official image
with its native env vars, plus the recommended `nofile` ulimits. It
reuses the same named volume as before: a `data-paths.xml` config
override points ClickHouse at the `data/` subdirectory of the volume,
which is exactly the layout the Bitnami image used, so old volumes work
in place (including SQL-created users) and fresh installs get the
identical layout. The service follows the required-secrets model:
`CLICKHOUSE_PASSWORD` must be set, matching the other services.

**Helm chart**: the Bitnami ClickHouse subchart is replaced by a
chart-owned single-node StatefulSet and Service running the official
image (non-root, HTTP `/ping` probes, config overrides mounted into
`config.d`, and the same `data-paths.xml` layout compatibility). On
upgrade, the chart automatically adopts the data PVC left behind by the
old subchart (`data-<release>-clickhouse-shard0-0`) via `lookup`, and
`fsGroup` relabeling handles the uid change on first mount. Both the
ClickHouse server and the webapp read the password from the same
chart-managed datastore secret (auto-generated and retained across
upgrades), so the server credential and the app's connection URL always
match. Existing `clickhouse.*` values keep working: `auth` (including
`existingSecret`/`existingSecretKey`), `persistence` (including
`global.storageClass`), `resources`, `secure`, `external.*`,
`configdFiles`, and now `nodeSelector`/`tolerations`/`affinity`.
Bitnami-only keys (`shards`, `replicaCount`, `keeper`,
`resourcesPreset`) are gone; default `resources` requests/limits match
what the old preset applied. The docs state the 25.8 minimum for
bring-your-own ClickHouse.

## Upgrade caveats

An adversarial review of the upgrade path found a few cohorts that need
awareness (all documented):

- **GitOps tools that render with `helm template`** (no cluster access):
PVC auto-detection can't run, so `clickhouse.persistence.existingClaim`
must be set to the old PVC name or ClickHouse starts on a fresh empty
volume. Documented in the values file and the Kubernetes self-hosting
docs. Tools that run real helm installs (e.g. Flux) adopt automatically.
- **A pinned `CLICKHOUSE_IMAGE_TAG`** pointing at a Bitnami tag must be
updated to an official image tag; documented in the Docker self-hosting
docs.
- **Storage without `fsGroup` support** (NFS, hostPath): set
`clickhouse.volumePermissions.enabled: true` for a one-time
ownership-fixing init container.
- **Rollback is not automatic**: once the official image has run, file
ownership changes and the Bitnami image can no longer read the volume
without a manual chown, and ClickHouse does not support downgrades
across the version gap.

## Verification

- Full upgrade simulation for Compose, twice (before and after rebasing
onto the required-secrets release): booted the ClickHouse service from
the old compose file on `main` (Bitnami), wrote thousands of rows, then
brought the same project up with this branch's compose file. The
official 26.2 server came up healthy on the same volume with all rows
intact, SQL-created users working, and writes succeeding.
- Adoption scenarios tested against real containers: old volume + root
entrypoint (Compose), old volume owned by the Bitnami uid + non-root 101
with fsGroup-style group permissions (Kubernetes), and fresh volumes for
both.
- `helm lint`, `helm template` (default values, `existingClaim` set,
external ClickHouse, volumePermissions/scheduling toggles, and the
production example) and kubeconform all pass, mirroring the release CI
steps. The rendered webapp Deployment and ClickHouse StatefulSet resolve
to the same datastore secret key.
- Inserts using
`input_format_json_infer_array_of_dynamic_from_array_of_different_types`
(the setting that fails on 25.7.5) succeed on the upgraded volume.

## Upgrade preflight and docs

A production upgrade report on this branch surfaced two hazards that
predate this PR — both landed in chart 4.5.6 (#4316) — so they are fixed
here rather than left for the next person to hit.

**`secrets.existingSecret` gained two required keys.** The webapp
started reading `PROVIDER_SECRET` and `COORDINATOR_SECRET`, and when
`existingSecret` is set the chart generates nothing, so a missing key
only surfaced as a `CreateContainerConfigError` partway through the
webapp rollout. The pre-install/pre-upgrade validation now looks the
Secret up and fails with the complete list of missing keys, leaving the
running release untouched. It is skipped under `helm template` and
client-side dry-run, where `lookup` cannot read the cluster.

**Bundled datastore credentials moved into the chart-managed Secret**
(`<release>-clickhouse`/`admin-password` →
`trigger-datastore`/`clickhouse-admin-password`). The chart wires both
ends itself, but consumers outside it — maintenance CronJobs, Grafana
datasources, secret syncs — have to be repointed. A new `## Upgrading`
section in the Kubernetes docs carries the old→new mapping, the two new
keys, and a pointer to the ClickHouse image notes.

The existingSecret key list in the docs also named
`OBJECT_STORE_ACCESS_KEY_ID`/`OBJECT_STORE_SECRET_ACCESS_KEY`, which are
env var names rather than keys the chart reads; corrected to the real
key names and the condition under which they apply.

Verified on a throwaway kind cluster with `--dry-run=server`: a
pre-4.5.6 Secret fails with both key names listed, the documented
`kubectl patch` clears it, and default values, `existingClaim`, external
ClickHouse, volumePermissions/scheduling and the production example all
still render. A real `helm install` followed by an upgrade against an
incomplete Secret aborts with the release still at revision 1 and
`deployed`. `helm lint`, the CI render and kubeconform (59 resources, 0
invalid) pass.

---------

Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
2026-08-01 14:17:27 +01:00
Eric Allam db6228dd1e chore(webapp,core,sdk): upgrade @s2-dev/streamstore to 0.25 and migrate S2 hosts (#4349) 2026-08-01 11:33:34 +01:00
claude[bot] 17d849b2d6 feat(cli): expose region option on the MCP trigger_task tool (#4439)
<!-- ccr-slack-attribution -->
_Requested by **Eric Allam** · [Slack
thread](https://triggerdotdev.slack.com/archives/C0BEM9Z73TM/p1785491472104199)_

## Checklist

- [ ] 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.
- [ ] I ran and tested the code works

---

## Testing

Static checks only, all clean:

- `pnpm run typecheck --filter trigger.dev`
- `pnpm run format`
- `pnpm run lint`

No live task was triggered against a running project, so the "ran and
tested" box above is left unchecked.

---

## Changelog

**Before:** triggering a task through the MCP server always ran it in
the project's default region. There was no way to pick one.

**After:** the `trigger_task` tool accepts an optional `region` option,
so you can choose the region a run executes in.

**How:** `region: z.string().optional()` was added to
`TriggerTaskInput.options` in `packages/cli-v3/src/mcp/schemas.ts`. No
call-site change was needed — `tools/tasks.ts` passes `options` through
verbatim, and `TriggerTaskRequestBody.options.region` already existed.
The tool description in `docs/mcp-tools.mdx` gained a matching line, and
a patch changeset is included.

There is no batch-trigger MCP tool, so there is no sibling tool to
mirror this change on.

---

## Screenshots

N/A — no UI changes.

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-31 11:54:42 +01:00
Eric Allam 4eb9292cbe feat(webapp,run-engine): queue metrics and health dashboard (#4131)
## Summary

Three related changes, each independently gated:

**Queue metrics and health.** Per-queue depth, throughput (enqueued,
started, completed), concurrency, whether a queue is throttled, and
scheduling delay (how long a run waits between becoming eligible and
actually starting), plus a per concurrency-key breakdown for keyed
queues. Collected from inside the run queue itself, stored in
ClickHouse, and surfaced on the Queues list, a new per-queue detail
page, the task pages, and the run inspector. The question it answers is
"does this queue have enough concurrency to keep up, and if not, which
key or which limit is the constraint".

**Percent-based queue concurrency limits.** A queue's concurrency
override can now be expressed as a percentage of the environment limit,
stored as the source of truth and re-materialized whenever the
environment limit changes. Absolute overrides above the environment
limit are now **rejected with a 400** instead of being silently capped,
which is a behavior change on `POST
/api/v1/queues/:queue/concurrency/override`.

**The `health` report.** A server-computed verdict on whether work is
flowing, whether the runs that do start are healthy, and whether
telemetry is fresh, rendered as text with sparklines. Available as `GET
/api/v1/reports/:key`, `trigger report`, and the `get_report` MCP tool
(plus a `report` MCP prompt, which shows up as a slash command in hosts
that support prompts).

With the flags off, the Queues page renders the pre-metrics component
verbatim, nothing is emitted, and nothing is written to ClickHouse.

## Configuration

Two independent gates, on purpose. Emission is global so data accrues
for everyone before anyone can look at it; the view is per organization
so it can be turned on for one org at a time without a deploy.

**Runtime flags (no restart)**

| Flag | Store | Gates |
| --- | --- | --- |
| `queue_metrics:enabled` | run-queue Redis key (`"1"`/`"0"`, off by
default) | All emission, gauges and counters. Cached in-process for 10s
with stale-while-revalidate, warmed eagerly at boot so the first op
after a deploy is not dropped. |
| `queue_metrics:gauge_sample_rate` | run-queue Redis key, `0..1` |
Fraction of queue ops that emit a gauge. Counters are never sampled, so
throughput stays exact at any rate. |
| `queueMetricsUiEnabled` | feature-flag catalog: global `FeatureFlag`
row, per-org `Organization.featureFlags` override wins | Whether an org
sees the metrics view at all: the Queues list variant, the queue detail
route, the built-in Queues dashboard, the concurrency-keys endpoint, and
the metrics blocks on task pages and the run inspector. Off by default;
a gated org gets a 404 on the detail route rather than an empty page. |

Both Redis keys are readable and writable from `/admin/queue-metrics`
(super-admin UI, with a live per-shard stream-health table) and
`GET`/`POST /admin/api/v1/queue-metrics` (admin PAT). The admin surface
uses its own Redis client, so it works on any instance regardless of
whether that instance runs the emitter or the consumer.

**Environment variables (boot time)**

| Variable | Default | Notes |
| --- | --- | --- |
| `QUEUE_METRICS_EMIT_ENABLED` | `0` | Constructs the emitter and
injects it into the run engine. Without it the run queue has no emitter
at all. |
| `QUEUE_METRICS_CONSUMER_ENABLED` | `0` | Boots the stream consumer on
this instance. Independent of emission, so consumers can be sized
separately from the API. |
| `QUEUE_METRICS_STREAM_SHARD_COUNT` | `4` | Stream shards, hashed per
queue. |
| `QUEUE_METRICS_CONSUMER_BATCH_SIZE` | `1000` | Poll batch equals
insert batch, so an ack can never outrun a write. |
| `QUEUE_METRICS_REDIS_{HOST,PORT,USERNAME,PASSWORD,TLS_DISABLED}` |
falls back to the run-queue Redis | Set `HOST` to move the metrics
stream onto a dedicated instance so a metrics backlog cannot compete
with the run queue for memory. Self-hosters can leave it unset and get a
single-Redis deployment. |
| `QUEUE_METRICS_COUNTER_STREAM_MAXLEN` | `2000000` shared, `8000000`
dedicated | Bound on how much a stalled consumer can hold. The default
is deliberately lower when the stream shares the queue-critical Redis. |
| `QUEUE_METRICS_COUNTER_ODOMETER_TTL_SECONDS` | `604800` | TTL on the
per-queue cumulative counter key, refreshed on every write, so only
queues idle for the whole window are purged. |
| `QUEUE_METRICS_MAX_QUEUE_NAMES_PER_ENV` | `1000` | Distinct queue
names tracked per environment; overflow collapses into `__overflow__`. |
| `QUEUE_METRICS_MAX_CONCURRENCY_KEYS_PER_QUEUE` | `10000` | Same idea
one level down, per queue. |
| `QUEUE_METRICS_GAUGE_SAMPLE_RATE` | `1` | Default for the live
sample-rate key above. |
| `QUEUE_METRICS_QUERY_TABLES_VISIBLE` | `0` | Lists the queue-metrics
tables in the Query page, its schema docs, the schema API and the AI
query context. Off keeps them unlisted while the feature is dark; a
query naming them still runs either way. |
| `QUEUE_METRICS_CLICKHOUSE_URL` | falls back to the shared wiring |
Runs queue metrics on their own ClickHouse service: the consumer's
inserts and every queue-metrics read go through it, so a metrics-heavy
chart refresh never competes with runs-list or trace reads. Unset
reproduces the previous split exactly (inserts on `CLICKHOUSE_URL`,
reads on the query pool). |
| `QUEUE_METRICS_CLICKHOUSE_READER_URL` | the write URL | Reader split,
so the consumer's inserts can never land on a read endpoint. |
|
`QUEUE_METRICS_CLICKHOUSE_{KEEP_ALIVE_ENABLED,KEEP_ALIVE_IDLE_SOCKET_TTL_MS,MAX_OPEN_CONNECTIONS,LOG_LEVEL,COMPRESSION_REQUEST}`
| `1`, unset, `10`, `info`, `1` | Pool tuning, matching the other
per-workload ClickHouse clients. |

Migrations to apply: ClickHouse `036_create_queue_metrics_v1.sql`, and a
Postgres migration adding the nullable
`TaskQueue.concurrencyLimitOverridePercent`. Both are additive.

## How collection works

Queue operations produce two kinds of signal, and they have opposite
failure modes, so they are handled differently.

**Gauges** (queued, running, queue limit, env queued, env running, env
limit, throttled, plus keys-with-backlog and worst-key wait on keyed
queues) are read *inside* the same Redis script that performs the
enqueue or dequeue, so the reading is atomic with the operation it
describes rather than a racy follow-up read. The script returns them on
its reply and the app forwards them to the stream. Gauges are sampled
and drop-tolerant: they are aggregated with `max`, so a lost reading
costs resolution, never correctness.

**Counters** (enqueued, started, completed, plus nack and dead-lettered)
are cumulative odometers. Each event increments a per-queue key on the
metrics Redis and emits the absolute total, and ClickHouse takes the
difference across buckets at read time. This is the important property
of the design: a summed-delta counter undercounts permanently on any
lost event, while a cumulative one self-heals, because the next
surviving reading restates the whole total. Only bucket granularity can
be lost, never the total. A queue returning after its odometer TTL
expired restarts at 1 and reset detection handles it, which is safe
precisely because expiry only spans a window with no activity.

Both land on one sharded Redis stream. A consumer reads it with a
consumer group, reclaims stale pending entries on a 15s interval rather
than on every poll, maps one entry to one or two ClickHouse rows
(whole-queue and, for keyed queues, per-key), and acks only after the
insert lands. Each batch carries a dedup token derived from its
stream-entry ids, and the target tables set
`non_replicated_deduplication_window`, so a retried batch cannot
double-count either the raw rows or the aggregates that hang off them.
Consumer and emitter both emit OTel metrics
(`queue_metrics.emitter.emitted`,
`queue_metrics.consumer.{entries,rows_inserted,insert_errors,insert_duration,stream_depth,group_lag,pending,lag_unknown}`);
stream depth and group lag are the two worth alerting on, and
`lag_unknown` exists because Redis can report a null lag after a trim,
which must not be read as zero.

## Storage and read path

`queue_metrics_raw_v1` is a short landing table with a 6 hour TTL. Four
aggregate tiers are materialized straight from raw, never cascaded off
each other, each with a 30 day TTL:

- `queue_metrics_v1`, 10 second buckets per queue, the default read path
- `queue_metrics_5m_v1`, 5 minute buckets per queue, for wide ranges and
cross-queue ranking
- `env_metrics_v1`, 10 second buckets per environment, queue-independent
so it stays cheap at any range
- `queue_metrics_ck_v1`, 10 second buckets per concurrency key

Every tier is an MV from raw because the counter states do not survive a
cascade: their merge is order sensitive, so a `-MergeState` chain off
the 10s table inflates the result, and the same property means an
aggregate state may only be merged inside one queue. That constraint is
now enforced by the query engine rather than by reviewer discipline: a
column can declare a `mergeGroupKey`, and any query that references it
without grouping by, or pinning to a single value of, every named key
fails to compile with an actionable message.

On the read side, TRQL gains three tables (`queue_metrics`,
`env_metrics`, and a `queue_metrics_by_key` that is hidden from the
editor, schema docs and schema API but still queryable, so per-key rows
can never silently merge into a plain per-queue query), plus
`deltaSumTimestampMerge` and `quantilesTDigestMerge`. Two schema-level
optimizations ride along: a table can declare coarser rollups, so a
query whose bucket interval is 5 minutes or wider is routed to the 5m
table with no change to the query itself, and it can opt into the
ClickHouse query cache with time bounds floored to a fixed grid, so the
auto-refreshing dashboards actually share cache entries instead of
missing on every tick. Both are caller-side substitutions, so the
printer stays unaware of physical layout.

All of this can also live on its own ClickHouse service. A table
declares the pool its reads run on, the three queue-metrics tables name
the dedicated one, and the ingestion consumer writes through the same
client, so both directions move together with one env var and nothing
else routes differently.

The other engine change is opt-in gap filling: charts can request rows
for empty buckets, where counters zero-fill and gauges carry forward.
Grouped gauge series are densified per group and carried inside a
partition, so a quiet queue's line holds its last value without bleeding
another queue's value into it.

## Queue concurrency limits

`concurrencyLimitOverridePercent` on `TaskQueue` is the source of truth
when an override is set as a percentage; the absolute `concurrencyLimit`
is materialized from it (floored, clamped to at least 1 so a percentage
can never act as a pause, and never above the environment limit). Every
path that changes an environment limit now recalculates the
environment's percent-based overrides afterwards, outside the
transaction, and pushes changed limits to the engine. The push is
attempted even when the stored value did not change, so a previously
failed sync self-heals rather than leaving the database and the engine
diverged; paused queues are skipped so a recalculation cannot
effectively unpause one.

The API accepts exactly one of `concurrencyLimit` or `percent`, and the
reject-instead-of-clamp change above means a request asking for more
than the environment allows now fails loudly. The percent bound (greater
than 0, at most 100) is defined once and shared by the zod schema, the
dashboard mutation handler and the service, so the three cannot drift.

The concurrency-keys table on a queue is now paginated against the
ClickHouse per-key tier, ranked by peak backlog with the total on every
row from a single scan, and only the keys on the current page are
enriched with live counts from Redis. That replaces a hard top-50 cap
with something whose cost is a function of page size rather than key
cardinality.

## The health report

`GET /api/v1/reports/:key?period=&format=markdown|ansi|json`. The
verdict is computed on the server and is deterministic, not
model-generated. Three independent analyzers run over one input
snapshot: flow (is work moving, and if not, is the cause a limit,
throttling, one bad queue, or dead-lettering), execution (are the runs
that start succeeding, and at what latency), and liveness (how fresh is
the telemetry). When telemetry is genuinely stale, the first two are
forced to unknown and every actionable field is stripped, so no surface
ever advises action off stale data.

Authorization is per query table rather than a blanket query grant: a
JWT must be scoped to every table the report reads (`runs`,
`env_metrics`, `queue_metrics`), so a narrowly scoped token cannot pull
a report that reads more than it was granted. `period` is validated as a
shorthand with a 90 day ceiling at the edge. The report catalog is a
registry of `{ load, interpret }` entries, so the next report is a new
entry and no change to the route, the view model, the renderers, the CLI
or the MCP tool.

`trigger mcp` no longer launches the install wizard when stdout is a
TTY, which fixed a real failure: hosts spawn the server over a PTY, so
the wizard would open and the client would time out waiting for a server
that never started. The wizard now needs `trigger mcp --install`.

## The part that is live regardless of every flag

The enqueue and dequeue scripts now return a 2-tuple so a gauge reading
can ride back on the reply. Every return site in the eight affected
scripts is wrapped, and a `nil` original is converted to `false` on the
way out, because a raw `nil` in the first slot would make Lua truncate
the multi-bulk reply and silently drop the gauge on the throttled and
empty-queue paths. The reply shape and the destructuring on the app side
are exercised on every queue operation whether or not metrics are
enabled, so that is the part of `run-engine` worth the closest review.

One behavior fix in the same area: the scheduling-delay anchor is set
only on a run's first entry into the queue. Anchoring it to trigger time
on re-enqueues made waitpoint and checkpoint resumes report the entire
wait as scheduling delay. Queue ordering is untouched, so a re-enqueued
run keeps its position, and nacks deliberately keep the original anchor
because a rolled-back dequeue is the same continuous wait.

A pending-version promotion still anchors to trigger time, on purpose:
that promotion is the run's first real entry into the queue, since the
trigger deliberately held it back waiting for a worker version, and the
TTL is armed at the same point for the same reason. The consequence is
worth naming, because it is a judgement call: a run that waits on a
deployment reports that wait as scheduling delay on its queue, which is
time unrelated to queue capacity.

## Verification

Unit and integration suites across the new package, the run queue, the
mapping layer, the query engine and ClickHouse (including a test that
applies migration 036 through the same splitter CI uses, and a
regression test that inserts the same batch three times to prove the
aggregates do not inflate). Beyond that, the whole path was driven end
to end against a live stack with real runs: emitter to Redis stream to
consumer to ClickHouse to the dashboards, for both the local dev path
and the deployed path where a supervisor drives the dequeue, with
assertions on exact counter reconstruction per queue and per concurrency
key, throttling, environment saturation, scheduling delay, and a
deliberate mid-stream reading drop to confirm the cumulative counters
still reconstruct the correct total. The gated-off state was checked on
every touched surface.

The dedicated ClickHouse service was verified against a second,
separately-schema'd instance: with it configured, the driven counters
reconstruct exactly on the dedicated instance, the shared instance gains
no rows for that window, a read through the query API returns the value
that exists only on the dedicated instance, and a `runs` query still
succeeds (it would fail outright if it were mis-routed to a service
without that table). With the variable unset, the full suite passes
unchanged.

---------

Co-authored-by: Katia Bulatova <katia@trigger.dev>
Co-authored-by: Katia Bulatova <katherine.bulatova@gmail.com>
Co-authored-by: James Ritchie <james@trigger.dev>
2026-07-29 16:45:24 +01:00
DKP 8d321f8d6e docs: give docs pages unique title tags and redirect stale pages (#4416)
## Summary

Several docs pages rendered identical `<title>` tags, which weakens
search indexing and makes results ambiguous. Each affected page now has
a unique, descriptive title while keeping its existing sidebar label
unchanged.

Alongside the retitles:

- Removed two stale build-system upgrade pages that were no longer in
the navigation, with redirects to the current package upgrade guide.
- Redirected the build-extensions group index to its overview page so
the two URLs stop sharing a title.
- Dropped a leftover orphaned API reference page (its old URL already
redirects to the management overview).

No links break: nothing in the docs points at the removed pages, and
every redirect target exists.
2026-07-29 12:09:45 +01:00
Matt Aitken 205bdc3103 docs(wait): separate compute billing from concurrency release (#4405)
The wait docs describe the 5 second compute-billing threshold as if it
were also the suspension threshold. It isn't, and the gap is confusing
when you're sizing a poll interval:

- **Compute** stops being charged for any wait longer than 5 seconds.
- **Concurrency** is only released once the machine has been snapshotted
and shut down. For `wait.for` and `wait.until` that happens 60 seconds
into the wait — a shorter wait stays `EXECUTING` and holds its
concurrency slot for the whole wait, even though the compute is free.

So `await wait.for({ seconds: 30 })` in a polling loop never releases
its slot, which looks like a bug if the docs told you waits over 5
seconds checkpoint.

## Changes

**`docs/snippets/paused-execution-free.mdx`** — rendered on `/wait`,
`/wait-for` and `/wait-until`. Drops "we checkpoint and" from the
billing sentence so it's purely about compute, then adds one paragraph
for the concurrency half.

**`docs/queue-concurrency.mdx`** — the "Waits and concurrency" section
states flatly that waiting runs don't consume slots. Adds a short
subsection for the time-based exception.

**`docs/how-to-reduce-your-spend.mdx`** — "Waits longer than 5 seconds
automatically checkpoint your task, meaning you don't pay for compute" →
the compute claim only. Code comments follow, plus a pointer that
waiting doesn't always free concurrency.

**`docs/how-it-works.mdx`** — the Checkpoint-Resume walkthrough used
`wait.for({ seconds: 30 })` as *the* example of a wait that suspends.
Bumped to 5 minutes and noted the sub-60s exception.

No behaviour change — docs only.
2026-07-28 15:20:00 +01:00
Chris Arderne 55a3bf2858 feat(core,cli): add node-24 and node-26 runtimes, deprecate experimental aliases (#4337)
## Summary

Adds `node-24` and `node-26` as first-class `runtime` options in
`trigger.config.ts`. Previously these Node versions were only reachable
via the `experimental-node-24` / `experimental-node-26` names.

Those experimental names are now **deprecated aliases**: they still
resolve to `node-24` / `node-26` for backwards compatibility, but
loading a config that uses them prints a deprecation warning pointing at
the new name.

```ts
export default defineConfig({
  runtime: "node-24",
  project: "<your-project-ref>",
});
```

## Details

- `ConfigRuntime` (the public config schema) now accepts `node-24` and
`node-26` directly; the internal `BuildRuntime` already supported them,
so base images and the deploy path are unchanged.
- `resolveBuildRuntime` passes the new names straight through and keeps
mapping the experimental aliases to their replacements.
- Renamed the runtime helper from `isExperimentalConfigRuntime` to
`isDeprecatedConfigRuntime` and added `deprecatedRuntimeReplacement` so
the CLI can name the replacement in its warning.
- Docs snippet updated to list the new versions and flag the deprecated
names.
2026-07-22 14:10:27 +01:00
Matt Aitken e2d3b8388c feat(sdk): return lastEventId from writeTurnComplete and typed capture result (#4304)
## Summary

Two ergonomic additions for custom chat-agent loops that own the turn
loop (`chat.customAgent`, `chat.createSession`, and the hand-rolled
primitives).

`chat.writeTurnComplete()` now resolves to `{ lastEventId }`, the resume
cursor for the start of the next turn. A custom loop can persist it
straight from the task instead of round-tripping it back from the client
after the turn ends. The value was already produced internally by the
turn-complete write; the public wrapper simply discarded it.

`chat.pipeAndCapture()` no longer throws when a stream is stopped or
fails. It now resolves to a `PipeAndCaptureResult` carrying any partial
`message` captured before the stop or failure, a typed `status`
(`"complete" | "aborted" | "error"`), and the `error` on failure.
Previously a failed stream threw and the partial was lost, and an abort
was captured only when the AI SDK happened to fire `onFinish` in time.

```ts
const { message, status, error } = await chat.pipeAndCapture(result, { signal });
if (message) conversation.addResponse(message);
if (status === "error") logger.error("turn failed", { error });

const { lastEventId } = await chat.writeTurnComplete();
await db.chats.update(chatId, { lastEventId });
```

## Design

`pipeAndCapture` wraps the pipe in a `try/catch` and classifies the
outcome from the abort signal (a stop drains the source stream cleanly
rather than throwing) versus a thrown error. It also races the
`onFinish` capture against a timeout so a hard stop that prevents
`onFinish` from firing can't hang the caller. This mirrors the capture
path `chat.agent` already uses internally.

The `finishReason` from `onFinish` is surfaced too, since it was already
captured on the built-in path.

The internal `turn.complete()` helper keeps its existing contract: it
still returns `UIMessage | undefined`, still throws on a genuine stream
failure, and still discards output on a full run cancel.

## Breaking change

`chat.pipeAndCapture` previously resolved to `UIMessage | undefined`.
Call sites now read `.message` off the result. This is a young,
low-level API; the docs examples are updated in this PR.
2026-07-21 17:08:30 +02:00
Chris Arderne 6997aeb05e fix: security release 2026-07-08 (#4316)
⚒️ Publish Worker (v4) / build (supervisor) (push) Has been cancelled
2026-07-21 12:00:58 +01:00
Matt Aitken 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)
2026-07-14 13:59:15 +01:00
Iss 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
2026-07-13 20:01:15 -04:00
Eric Allam 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.
2026-07-13 17:17:02 +01:00
Eric Allam 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.
2026-07-13 12:35:42 +01:00
Eric Allam 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.
2026-07-13 11:32:06 +01:00
Chris Arderne fda8e77175 fix(docs): openapi labels for different bulk api variants (#4223)
Replace Option 1 Option 2 etc with labelled variants.
2026-07-13 09:39:33 +01:00
Eric Allam 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.
2026-07-11 12:46:24 +01:00
Iss 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
2026-07-10 11:03:28 -04:00
Chris Arderne 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
2026-07-10 11:49:45 +01:00
Wes Mason 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`.
2026-07-10 11:31:42 +01:00
nicktrn 0631c8373c chore: retire legacy v3 dev websocket + delete legacy self-hosting docs (#4198)
Follow-up to #4194 (v3 execution app + core-helper removal). The v3
(engine V1) is end-of-lifed and enforced off in prod, so this removes a
self-contained slice of the remaining dead v3 code while **keeping every
user-facing deprecation message** - a user still on v3 must still be
told to upgrade.

## Legacy dev websocket

`app/v3/handleWebsockets.server.ts` backs the `/ws` transport used
**only** by the legacy v3 `trigger dev` CLI (v4 dev uses a different
transport). It's now authenticate-then-close with
`V3_DEV_DEPRECATION_MESSAGE`, so an old CLI is still told what to do -
only the legacy `AuthenticatedSocketConnection` / `DevQueueConsumer`
execution behind it (which can no longer run) is removed.

- Deleted `app/v3/authenticatedSocketConnection.server.ts` (its only
consumer).
- `engineDeprecation.server.ts` and the deprecation message constants
are untouched.

## Docs

Deleted the intentionally-legacy "Docker (legacy)" self-hosting page
(`open-source-self-hosting.mdx`) and redirected
`/open-source-self-hosting` (+ the existing
`/v3/open-source-self-hosting` alias) to `/self-hosting/overview`;
repointed the two inbound links. The current `self-hosting/*` docs
already describe the v4 (single supervisor) setup.

## Deliberately out of scope

Despite the branch name, this PR does **not** touch MarQS or the
socket.io coordinator/provider namespaces. Investigation found MarQS is
entangled with **live v2** queue/metrics/concurrency/project-cleanup
code (`runQueue`, `queueSizeLimits`, `taskRunConcurrencyTracker`,
`EnvironmentQueuePresenter`, `registerProjectMetrics`, `deleteProject`),
so it needs a per-file reviewed pass, not a bulk delete. That remainder
stays on TRI-11883.

refs TRI-11883
2026-07-09 14:40:44 +01:00
Matt Aitken 71e4b00880 docs: add ClickHouse chat agent example project page (#4195)
## What

Adds a new Example projects page: **ClickHouse chat agent** — a
`chat.agent()` that answers questions about your data by writing and
running SQL against ClickHouse Cloud via the official Node.js ClickHouse
client.

The page follows the existing example-project format (overview,
features, GitHub repo card, how-it-works with code excerpts, relevant
code links) and is registered in `docs.json` in alphabetical order.

## Note

The GitHub repo card links to
`triggerdotdev/examples/tree/main/clickhouse-chat-agent`, which lands in
a companion examples PR — merge that one first.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-09 11:31:21 +01:00
Chris Arderne 7a19bb4cbb chore: add security docs (#4192)
Adds SECURITY.md and security page to the docs.
2026-07-08 17:23:18 +01:00
Matt Aitken a682f1d171 feat(hosting): default self-hosted realtime streams to v2 (s2-lite) (#4185)
## Summary

Realtime streams (AI-agent token streaming and run streams) now default
to v2 for self-hosters, backed by a bundled [s2-lite](https://s2.dev)
service. Self-hosting previously shipped no S2 configuration, so streams
ran on the Redis-backed v1 path and there were no docs for wiring up v2.
Both the Docker Compose stack and the Helm chart now provision s2-lite
with persistent storage and set the stream env vars out of the box.

## What's included

- **Docker Compose**: a persistent `s2` service (s2-lite), a basin init
spec, and the `REALTIME_STREAMS_S2_*` plus
`REALTIME_STREAMS_DEFAULT_VERSION=v2` env on the webapp. `.env.example`
documents the v1 fallback and hosted-S2 options.
- **Helm**: an `s2` StatefulSet, PVC, Service and ConfigMap (runs as the
non-root image user via `fsGroup`), an `s2` values block, and webapp env
wiring with an existing-secret path for hosted S2.
- **Docs**: the `REALTIME_STREAMS_S2_*` and
`REALTIME_STREAMS_DEFAULT_VERSION` vars in the webapp env reference,
plus a "Realtime streams" section in the Docker and Kubernetes
self-hosting guides.

## Notes

- The OSS code default stays `v1`; v2 becomes the default purely through
the self-hosting artifacts, so non-self-host deployments are unaffected.
Disabling s2, or setting the version back to `v1`, cleanly reverts to
Redis-backed v1.
- With v2 enabled, the bundled s2 service is a required dependency for
streaming: if it is down, streams error while the task itself still
runs. That is the intended trade for the better v2 path.
- You can point at a hosted S2 at s2.dev instead of the bundled server.
2026-07-08 14:58:37 +01:00
Katia Bulatova e0bf74bfae docs: billing limits and alerts page (#4132)
New `/billing-limits` page covering the full [billing limits
feature](https://trigger.dev/changelog/billing-limits) : the three limit
options (plan / custom / no limit), billing alerts (% of limit or dollar
thresholds), what happens when the limit is reached, the recovery flow,
the soft-limits caveat, and the billing limit marker on the Usage page.
2026-07-08 13:46:42 +02:00
Eric Allam fbd86b6ee9 feat(sdk): onEvent observability callback on the chat transport (#4187)
## Summary

`sendMessage` from `useChat` gives no feedback about whether a message
actually reached the backend, and the `fetch` override is wire-level: it
requires knowing endpoint semantics, cannot attribute requests to
messages, and misses the headStart first-turn POST entirely. This adds a
typed `onEvent` observability callback to `TriggerChatTransport` /
`useTriggerChatTransport` so send-success metrics, time-to-first-token,
and "sent but never answered" watchdogs become a few lines of client
code.

## Example

```ts
const transport = useTriggerChatTransport({
  task: "my-chat",
  accessToken: ({ chatId }) => mintChatAccessToken(chatId),
  onEvent: (event) => {
    switch (event.type) {
      case "message-sent":
        // Durably acknowledged by the session's input stream, not just "request accepted".
        metrics.increment("chat.message_sent", { source: event.source });
        metrics.timing("chat.send_duration_ms", event.durationMs);
        break;
      case "message-send-failed":
        metrics.increment("chat.message_send_failed", { status: event.status });
        break;
      case "first-chunk":
        metrics.timing("chat.ttft_ms", event.sinceSendMs ?? 0);
        break;
      case "turn-completed":
        metrics.timing("chat.turn_duration_ms", event.sinceSendMs ?? 0);
        break;
    }
  },
});
```

## Design

One callback, one discriminated union (`ChatTransportEvent`):

- `message-sent` / `message-send-failed`: terminal send outcomes with
`messageId`, a `source` discriminator (submit, regenerate, steer,
action, stop, head-start), `durationMs`, `bodyBytes`, the append's
idempotency key (`partId`, also stored on the server-side record), and
error + HTTP status on failure. `message-sent` means the append was
durably acknowledged, after any internal token-refresh retries.
- `stream-connected` (with a `resumed` flag and the cursor it connected
from), `first-chunk` (chunk type plus `sinceSendMs` for
time-to-first-token), `turn-completed` (`sinceSendMs` full-turn latency
and the agent's committed input cursor), and `stream-error` follow the
response side, so a send can be paired with the answer that should
follow it. `messageId` on response events is client-side attribution
from the last turn-producing send on that chat.

Emissions sit at the transport's existing choke points, covering every
send path uniformly (including steering and headStart, which the fetch
override cannot observe). Exceptions thrown by the callback are
swallowed: observability can never break the chat. The React hook keeps
the callback live across renders instead of freezing the first-render
closure.

## Verification

Unit tests drive the transport directly with the `fetch` override as the
network stub (send success/failure per source, stream lifecycle, resumed
flag, field enrichment, callback exceptions swallowed). Verified
end-to-end against a realistic metrics setup in the ai-chat reference
app (counters, send-duration and TTFT histograms, and both watchdogs
built purely on these events): a healthy two-turn chat produces exactly
the expected event sequence and TTFT values; an oversized append records
`message_send_failed` with status 413; and killing the worker after a
durable send fires both `sent_but_no_stream` and `sent_but_unanswered`,
reproducing and detecting the "message disappeared" failure mode that
motivated this feature.
2026-07-08 11:01:40 +01:00
Chris Arderne aa74e68c71 feat(sdk): add bulk replay to api and sdk (#4105)
## Summary

Adds SDK and API support for run bulk actions. You can now create bulk
cancel or replay actions from `@trigger.dev/sdk` using run IDs or the
same filters as `runs.list()`, then retrieve, list, poll, or abort the
action by its `bulk_` handle.

Tests, docs, changesets added.

## Design

The dashboard bulk action service now accepts structured filters instead
of reading directly from a dashboard request, so the dashboard and API
share the same creation path. Replay actions created through the API are
attributed with the existing `api` trigger source, while
dashboard-created actions keep `dashboard`.

The SDK exposes the new surface under `runs.bulk.*`, including
`targetRegion` for replay region overrides and cursor pagination for
listing bulk actions.

## Filters and runIds

Nuance on filters. If `filter` is provided, it MUST have at least one
key. This is to remove the footgun of passing no filter and selecting
all runs.

```typescript
   { action: "cancel", runIds: ["run_1"] } // valid
   { action: "cancel", runIds: [] } // invalid, min(1)
   { action: "cancel", filter: { status: "FAILED" } } // valid
   { action: "cancel", filter: {} } // invalid
   { action: "cancel", filter: {}, runIds: ["run_1"] } // invalid
```
2026-07-07 15:43:30 +01:00
Eric Allam add0a7da0a fix(sdk,core): stop chat sessions dropping messages that arrive during a turn (#4176)
## Summary

Sending a message to a chat whose run had ended could make the message
vanish: the continuation run replayed already-answered messages, never
processed the new one, and a page refresh lost it entirely. Chasing that
report surfaced four composing message-loss bugs in the chat session
runtime; this PR fixes all of them, each with a regression test.

## The fixes

1. **Stale resume cursor.** Records delivered while a run was suspended
(the waitpoint path) advanced the SSE resume counter but not the
committed-consume cursor, so the `session-in-event-id` header stamped on
turn-completes went stale by one record per suspended turn. Continuation
boots seed from that header, which is what made them replay
already-processed messages. `session.in.wait()` now advances both
cursors.

2. **Only the first buffered message dispatched.** Messages arriving
during a turn are consumed into a buffer whose end-of-turn pickup
dispatched only the first entry; the buffer was recreated each turn, so
the rest were discarded, and since consuming a record commits the cursor
the loss was permanent. A continuation boot's replay delivers several
records back-to-back, which put the user's new message at index 1 or
later. The buffer now outlives the turn and drains one message per turn
in both `chat.agent` and `chat.createSession` (whose equivalent buffer
was never read at all).

3. **Post-stop window in `chat.createSession`.** The turn's message
listener stayed attached through the stopped turn's post-stream work, so
a message sent shortly after stopping a turn was consumed into the dead
steering queue and lost. The listener now detaches when the stream
settles, matching the `chat.agent` loop.

4. **Handler leak on errored turns.** A turn that threw outside the
streaming section (for example from an `onTurnStart` hook) leaked its
message listener. Previously that silently lost mid-turn messages; with
the loop-level buffer it would have duplicated them instead. The
subscription handle is now detached by the turn's catch/finally, and
`chat.createSession` defensively detaches its prior turn's listener when
user code exits a turn without `complete()`/`done()`.

## Verification

Reproduced end-to-end with the ai-chat reference project before the fix
(message consumed but never answered, two replayed turns, gone on
refresh) and verified after (single clean turn, survives refresh,
turn-complete cursors strictly advancing). Regression tests in
`packages/trigger-sdk/test/pending-message-drain.test.ts` cover all
four, each verified red against the unfixed behavior. A smoke sweep of
the standard chat scenarios (basic send, multi-turn, suspend/resume,
mid-stream refresh, stop, steering, cancel + continue, and the
`createSession` variant) passes on the final branch state.
2026-07-07 14:10:46 +01:00
Chris Arderne 5158ee8ec8 chore: use latest self-host compose images (#4140)
## Summary

Depends on #4136.

Docker Compose self-hosting now uses the maintained `latest` image tag
by default instead of the frozen prerelease tag. The version-locking
docs keep pointing production users at explicit versioned tags when they
want pinned upgrades.
2026-07-06 22:03:16 +01:00
Chris Arderne 2ad39443be chore: update self-host image release references (#4136)
## Summary

Stable v4 Docker image builds now also publish `v4` and `latest` tags,
giving Docker-based self-hosters a maintained floating tag to use after
a stable release. The Kubernetes guide now uses the current Helm chart
line and current pinned examples, so new installs and upgrades resolve
to the 4.5 chart line instead of the 4.0 line.

## Design

The publish workflows add the floating tags only for stable `v4.x.x`
image tags. Prerelease and `main` builds keep their existing tags.
2026-07-03 13:38:19 +01:00
Eric Allam 4536eded9e docs: drop release-candidate framing ahead of the 4.5 GA (#4100)
Removes the release-candidate framing from the docs for the 4.5 GA: the
`@rc` install caveats on the `skills` CLI command, and the AI Agents and
Prompts release-candidate banner (a shared snippet used across the
ai-chat and prompts pages) plus the `>=4.5.0-rc.0` compatibility pin in
the AI reference.
2026-07-02 11:11:12 +01:00
Eric Allam 9e14c6ff11 docs(ai-chat): add the 4.5.0 GA changelog entry (#4033)
## Summary

Adds the `4.5.0` GA entry to the AI chat agents changelog covering the
`chat.agent` changes in that release: a new `apiClient` option on
`chat.headStart` and `chat.createStartSessionAction` for pointing chat
sessions at a different project or environment, the fix for messaging
chat agents deployed to a preview branch, and the fix for Head Start
handovers when the agent also defines a `prepareMessages` hook.

All four changes are from
[#4018](https://github.com/triggerdotdev/trigger.dev/pull/4018).
2026-07-02 11:10:09 +01:00
Katia Bulatova cc752cc375 fix(webapp): fetch run-scoped trace subtrees for large traces (#4024)
Fixed trace rendering for child and nested runs in large traces.
Dashboard and trace API responses now load the requested run's trace
subtree instead of depending on the run span appearing in the initial
trace slice.
2026-06-30 13:35:31 +02:00
DKP 2cc1743c99 docs(skills): note the skills CLI command is only in the release candidate (#4047)
## Summary

The `skills` installer command (`npx trigger.dev@latest skills`) ships
in the release candidate but is not yet on the stable release, so
running it with `@latest` fails today. Adds a warning on each docs page
that shows the command, telling users to run it with the `@rc` tag until
it lands in `@latest`.

The commands themselves stay on `@latest`, so nothing needs reverting
once the next stable release ships, just the warnings.

Pages updated:
- `/skills`
- `/mcp-agent-rules`
- `/building-with-ai`
2026-06-26 18:15:06 +01:00
Katia Bulatova b1987dc090 feat(webapp): billing limits — pause, reject, recovery, and settings UI (#3996)
## Summary

Adds Billing Limits to the webapp.

Customers can set a monthly spend cap. When usage crosses the limit,
billable environments enter a grace period. If the limit is not resolved
before grace expires, new triggers are rejected until the organization
increases or removes the limit.
2026-06-26 17:12:53 +02:00
Chris Arderne b54201f986 chore: switch to oxfmt, oxlint - add ci checks (#3977) 2026-06-26 12:19:29 +01:00