Trigger.dev v4.7.0

Use multiple concurrency limits at once, restrict per-tenant concurrency with a total limit, and 4 bug fixes.

1 new feature, 10 improvements, 4 bug fixes, and 18 server changes.

Highlights

Concurrency limits, separated from queues

Concurrency used to be a property of the queue. It is now its own option on the task, and the simple case is a straight swap:


// Before
export const summarize = task({
id: "summarize",
queue: { concurrencyLimit: 5 },
run: async (payload) => {},
});
// After
export const summarize = task({
id: "summarize",
concurrency: { total: 5 },
run: async (payload) => {},
});

Per-tenant limits, now with a total

When runs were triggered with a concurrencyKey, the old single number became a per-key limit, and there was no way to also cap the task as a whole. Ten active tenants under a limit of 1 could run ten at once, a thousand could run a thousand. The new shape names both bounds, and you can set them together:


// Before: 1 per tenant, no ceiling on the total
export const generateReport = task({
id: "generate-report",
queue: { concurrencyLimit: 1 },
run: async (payload) => {},
});
// After: 1 per tenant, and at most 10 across all tenants
export const generateReport = task({
id: "generate-report",
concurrency: { perKey: 1, total: 10 },
run: async (payload) => {},
});

perKey caps each concurrencyKey pool, with runs triggered without a key sharing one pool. total caps every run of the task together, keyed or not.

More than one limit per task

Sharing a limit across tasks used to mean putting them on the same queue, which also merged their ordering. Now you declare a named limit with concurrencyLimit() and give it to every task that should share it. Each task keeps its own queue. A task can hold up to two named limits plus one inline shape, and a run starts only when every limit it holds has capacity:


import { concurrencyLimit, task } from "@trigger.dev/sdk";
// at most 25 concurrent runs across every task that holds this limit
export const openaiLimit = concurrencyLimit({ name: "openai", total: 25 });
export const generateSummary = task({
id: "generate-summary",
// at most 5 of this task at once, and also counts towards the shared openai limit
concurrency: [{ total: 5 }, openaiLimit],
run: async (payload) => {},
});
export const generateTitle = task({
id: "generate-title",
concurrency: openaiLimit,
run: async (payload) => {},
});

A named limit with only perKey caps each tenant across every task that holds it, which is how you cap a tenant's usage of your whole system rather than one task. A trigger call can swap a run's named limits with its own concurrency option.

The new Concurrency docs walk through the use cases: capping a task, sharing a limit between tasks, per-tenant limits, a per-tenant cap across many tasks, a global cap on a shared resource, and switching limits at trigger time.

The queue-level concurrencyLimit option keeps working unchanged and is deprecated in favor of concurrency. queues.overrideConcurrencyLimit and queues.resetConcurrencyLimit are deprecated too and only act on queues that still carry their own limit.

Manage limits at runtime with the new concurrencyLimits namespace. list() and retrieve(name) report each limit's bounds plus its live running and queued counts. override(name, { perKey, total }) changes only the bounds you pass, and reset(name) restores the declared values. A task's inline limit is addressed as task/<task-id>. Pause and resume a limit without touching its bounds:


import { concurrencyLimits } from "@trigger.dev/sdk";
await concurrencyLimits.pause("openai");
await concurrencyLimits.resume("openai");

Queue reads (queues.list() and queues.retrieve()) now include a version field. V1 queues keep their own concurrencyLimit. V2 queues, declared with concurrency, report concurrencyLimit as null and manage limits through concurrencyLimits.

Enforcement happens server-side. Trigger.dev Cloud enforces these limits today; self-hosted servers need the v4.7.0 image. Older servers accept the option without enforcing it. (ea97581, fa94feb, 414e5a2)

Improvements

  • Chat agents can now scope concurrency per session. Pass concurrencyKey (for example, a chat ID or tenant ID) and trigger-time named limits via triggerConfig.concurrency when starting a session from chat.createStartSessionAction, AgentChat, or a handover. Keys are never defaulted, so a session without one shares the task's keyless pool. (ea97581)
  • Chat streams now report Stream stalled: no records received after five retries of a connected stream that sends no records. Network failures and browser wakeups still recover automatically. Healthy tool calls with no records for about six minutes also hit this limit. Watch subscriptions remain unlimited, and caller cancellation still closes cleanly. (#4949)
  • Webhook verifier artifacts can now declare the provider's response contract as data: a handshake respondStatus, status codes for accepted deliveries and rejected signatures, and a GET verification flow (getHandshake) for providers that confirm a callback URL with a challenge. HMAC verifiers can read the timestamp from a body field, and the dashboard's test-send re-signs a recorded sample as of now so it passes the provider's replay window. (5f54fb2)
  • Added an optional onSettled callback to TriggerChatTransport.sendAction() so callers can confirm their action's input was processed, independently of whether the response stream closes. (#4956)
  • Chat agents now return to a durable wait when a session wake does not deliver a matching message. Resumed runs no longer stay active until their maximum duration, and the configured turn timeout survives repeated wakes. (e6ec2c4)
  • Steering messages now remain in context across agent steps and keep their original position in saved conversations, including custom response data written between steps. (5619acf)
  • Set up a new Trigger.dev account from the CLI. Login can prefill an email address, save your name after authorization, and resume later. init can create your first organization and project before scaffolding the app. (fb3b26d)
  • Added a submit_feedback MCP tool so coding agents can report a confusing tool error, a docs mismatch, or a missing capability without you filing it by hand. Turn it off with --skip-telemetry or TRIGGER_TELEMETRY_DISABLED; the tool is hidden while telemetry is off. (9d38ff5)
  • trigger promote now accepts environment API keys supplied through TRIGGER_ACCESS_TOKEN. Environment API key commands use the saved profile API URL when you pass no override. (562c943)
  • Zod 4 z.date() fields now convert to date-time strings in JSON Schema without weakening validation for other unsupported types. MCP tool discovery no longer fails when a tool input schema contains a date. (f8babdf)

Bug fixes

  • Fixed stale and empty project environment values in trigger dev, and added support for empty values in syncEnvVars(). (f384e83)
  • Fixed warm starts silently failing for deployments built with 4.6.0 to 4.6.3 in projects that resolve zod to a 3.x release. The runner could not parse the run handed to it by the warm-start service and exited, leaving the run waiting until the platform redrove it a few minutes later and started it cold. Redeploy to pick up the fix. (#4972)
  • Fixed a ~6-second delay between a task finishing and its run completing, and a ~31-second delay when cancelling a run, in projects that use zod 4.4 or newer. Run cost and billed usage were not affected. (#4980)
  • Fixed Windows deploys failing at indexing with Cannot find module on a percent-encoded path when the project directory contains spaces or non-ASCII characters. (b32c1d1)

Server changes

These changes are included in the v4.7.0 Docker image and are already live on Trigger.dev Cloud:

  • API rate limit usage is now recorded per environment in the metrics table as api.rate_limit.allowed, api.rate_limit.denied, api.rate_limit.remaining_min, api.rate_limit.limit.per_second, and api.rate_limit.limit.burst, so you can chart requests against your limit and 429s over time on the Query page and dashboards.
  • Archive queues you no longer use from the Concurrency page. Archived queues are hidden from the list and no longer count towards allocated concurrency.
  • Automatically archive preview branches after a configurable period without deployments, with protected branch names and a preview of affected branches.
  • Download a session's original saved transcript file from the session inspector.
  • Organization Owners and Admins can now choose, in Settings under Support Access, whether Trigger.dev support can open their dashboard directly or only after an Owner or Admin approves a request. Each approval lasts 7 days.
  • Allocating extra concurrency to environments now requires billing permissions, the same as purchasing it. Allocation changes are applied atomically, so simultaneous edits can no longer exceed your purchased concurrency.
  • Changing your account email now sends a confirmation link to the new address, and the change only takes effect once that link is opened. Requesting a magic link no longer creates an account until the link is used.
  • Removed the organization-wide Node.js 21 deprecation banner while keeping runtime upgrade guidance in Projects settings.
  • Creating a project now asks only for its name.
  • Batch waits no longer remain suspended when batch completion is briefly interrupted.
  • Batch-and-wait requests are rejected when the parent run belongs to another environment.
  • Dashboard pages now stay visible during network interruptions, with a disconnected banner and a Refresh button instead of a full-page error.
  • Deployments can replace declarative schedules at the schedule limit when the resulting set stays within quota.
  • "Deploy now" now tells you when the branch doesn't exist on GitHub instead of showing a generic error, and a first deployment no longer flags a harmless build-cache message as an error.
  • Support for empty-string environment variable values across the dashboard, API, and Vercel sync, behind a feature flag that is disabled by default.
  • Fixed a bug where a burst of telemetry could leave OpenTelemetry ingest rejecting every batch for a long time. Batches that wait too long are now dropped individually instead of restarting the processing workers, so ingest recovers as soon as the burst passes.
  • Scheduled task "Last run" times stay stable across unchanged deployments and align with configured schedule windows.
  • Team members and pending invites on the organization Team page are now listed in alphabetical order.

How to upgrade

Update the trigger.dev/* packages to v4.7.0 using your package manager:


npx trigger.dev@latest update # npm
pnpm dlx trigger.dev@latest update # pnpm
yarn dlx trigger.dev@latest update # yarn
bunx trigger.dev@latest update # bun

Self-hosted users: update your Docker image to ghcr.io/triggerdotdev/trigger.dev:v4.7.0.

Deploy your first agent today

Get set up in minutes, either on your own or with your coding agent.

Get started for free