Authoring Trigger.dev Tasks
Tasks are functions that can run for a long time with strong resilience to failure. Define them in files under your /trigger directory. Always import from @trigger.dev/sdk. Never import from @trigger.dev/sdk/v3 (deprecated alias) or @trigger.dev/core.
Setup
The run function receives the payload and a second argument with ctx (run context), an abort signal, and a deprecated init output. The return value is the task output and must be JSON serializable.
Core patterns
1. Validate the payload with schemaTask
schema accepts a Zod / Yup / Superstruct / ArkType / valibot / typebox parser or a custom (data: unknown) => T function. A validation failure throws TaskPayloadParsedError and skips retrying.
2. Configure retries and abort early
The default maxAttempts is 3. Throw AbortTaskRunError to stop retrying immediately. Task-level retry overrides the config-file defaults.
For finer control, catchError: async ({ payload, error, ctx, retryAt }) => {...} can return { skipRetrying: true }, { retryAt: Date }, or undefined (use normal logic). retry.onThrow, retry.fetch, also exist for in-task retrying.
3. Trigger another task and handle the Result
From inside a task use yourTask.triggerAndWait(payload). The result is a Result object that you must check (ok), or .unwrap() to throw on failure.
SubtaskUnwrapError carries runId, taskId, and cause. For fan-out use childTask.batchTriggerAndWait([{ payload: a }, { payload: b }]); the result has a .runs array, each entry { ok, id, output?, error?, taskIdentifier }.
4. Trigger from backend code with a type-only import
Outside a task, import the task type only and trigger by id. Do not import the task instance into backend bundles.
tasks.batchTrigger and batch.trigger([{ id, payload }]) cover batches. Trigger options include delay, ttl, idempotencyKey, idempotencyKeyTTL, debounce, queue, concurrencyKey, maxAttempts, tags, metadata, priority, region, and machine. Inspect runs with runs.retrieve, runs.cancel, and runs.reschedule.
5. Idempotency keys
idempotencyKeys.create(key, { scope }) returns a 64-char hashed key. A raw string key defaults to "run" scope (v4.3.1+); for once-ever behavior use scope: "global".
6. Waits and run metadata
wait.for({ seconds }) and wait.until({ date }) durably pause the run. metadata.* is readable and writable only inside run(); updates are synchronous and chainable (set, del, replace, append, remove, increment, decrement).
For human-in-the-loop, wait.createToken({ timeout, tags }) returns { id, url, publicAccessToken, ... }; resume with wait.forToken<T>(token: string | { id: string }) which returns { ok, output?, error? } (or .unwrap()), and complete it elsewhere with wait.completeToken(tokenId, output). Metadata max is 256KB and is not propagated to child tasks; push values to a parent with metadata.parent.* / metadata.root.*. (metadata.stream is deprecated since 4.1.0 in favor of streams.pipe().)
7. Scheduled (cron) tasks
The payload includes timestamp, lastTimestamp, timezone, scheduleId, externalId, and upcoming. Attach schedules dynamically with schedules.create({ task, cron, timezone?, externalId?, deduplicationKey }) (the dedup key is required and per-project), plus retrieve / list / update / activate / deactivate / del / timezones.
8. Queues and concurrency
Set queue: { concurrencyLimit } on a task, or share a queue across tasks:
At trigger time override with { queue: "queue-name" } and add concurrencyKey for per-tenant queues. Manage queues with queues.list / retrieve / pause / resume / overrideConcurrencyLimit / resetConcurrencyLimit.
9. trigger.config.ts essentials
build.external controls which packages stay out of the bundle. Build extensions (additionalFiles, prismaExtension, puppeteer, playwright, ffmpeg, pythonExtension, aptGet, syncEnvVars, etc.) come from @trigger.dev/build. telemetry configures instrumentations and exporters. Each extension has its own setup doc, all bundled under @trigger.dev/sdk/docs/config/extensions/ (start with overview.mdx); read the one you need before wiring it up rather than guessing the API.
Logging
logger.debug / log / info / warn / error(message, dataRecord?) write structured logs; logger.trace(name, async (span) => {...}) adds a span. Module-level metrics use otel.metrics.getMeter(name).
Common mistakes
-
CRITICAL: Treating the wait result as the output.
triggerAndWaitandwait.forTokenreturn a Result object, not the raw output.- Wrong:
const out = await childTask.triggerAndWait(p); use(out.foo); - Correct:
const r = await childTask.triggerAndWait(p); if (r.ok) use(r.output.foo);(or.unwrap()).
- Wrong:
-
Wrapping
triggerAndWait/batchTriggerAndWait/waitinPromise.all.- Wrong:
await Promise.all([childTask.triggerAndWait(a), childTask.triggerAndWait(b)]); - Correct:
await childTask.batchTriggerAndWait([{ payload: a }, { payload: b }]);(or a sequential for-loop).
- Wrong:
-
Importing the task instance into backend code.
- Wrong:
import { emailSequence } from "~/trigger/emails";in a route handler. - Correct:
import type { emailSequence }plustasks.trigger<typeof emailSequence>("email-sequence", payload).
- Wrong:
-
Calling
metadata.set/getoutsiderun().- Wrong: setting metadata at module scope or in unrelated backend code (a no-op;
getreturnsundefined). - Correct: call inside
run()or a task lifecycle hook.
- Wrong: setting metadata at module scope or in unrelated backend code (a no-op;
-
Assuming child tasks inherit the parent's queue or metadata.
- Wrong: expecting a subtask to share the parent's
concurrencyLimitor see its metadata. - Correct: subtasks run on their own queue; pass metadata explicitly via
{ metadata: metadata.current() }, or push up withmetadata.parent.*.
- Wrong: expecting a subtask to share the parent's
-
Bundling native/WASM packages.
- Wrong: leaving
sharp,re2,sqlite3, or WASM packages in the default bundle. - Correct: add them to
build.externalintrigger.config.ts.
- Wrong: leaving
-
Relying on a raw string idempotency key being global.
- Wrong:
trigger(p, { idempotencyKey: "welcome-email" })expecting once-ever (true only in v4.3.0 and earlier). - Correct:
await idempotencyKeys.create("welcome-email", { scope: "global" }).
- Wrong:
References
Sibling skills:
- trigger-realtime for subscribing to runs and triggering from the frontend with React hooks.
- trigger-authoring-chat-agent and trigger-chat-agent-advanced for building AI chat agents.
Reference docs ship beside this skill in the same package, read them locally (no network), pinned to your installed version. The sources: frontmatter above lists every doc this skill draws from, all under @trigger.dev/sdk/docs/. Start with:
@trigger.dev/sdk/docs/tasks/overview.mdx@trigger.dev/sdk/docs/triggering.mdx@trigger.dev/sdk/docs/config/config-file.mdx
Version
This skill is bundled inside @trigger.dev/sdk and read directly from node_modules, so it always matches your installed SDK version (see the adjacent package.json). The full documentation for these APIs ships alongside it under @trigger.dev/sdk/docs/.
