Trigger.dev 任务编写完全指南:从 task() 到 trigger.config.ts 的持久化任务实战
2026/9/21 14:21:31 网站建设 项目流程
  • AI Agent
  • 后端
  • 任务调度
  • 开发工具
  • 可观测性
  • AI 应用

【免费下载链接】trigger.dev

Trigger.dev – build and deploy durable AI agents and workflows

项目地址:https://gitcode.com/gh_mirrors/tr/trigger.dev
点击查看免费下载

Trigger.dev 的核心抽象是任务(Task)——一种可以长时间运行、对失败具备强韧性的函数。本指南以@trigger.dev/sdk技能文档trigger-authoring-tasks为骨架,系统讲解如何在/trigger目录下用task()/schemaTask()定义任务,并覆盖重试策略、Result 结果形态、幂等键、持久等待(wait)、运行元数据、cron 定时任务、队列与并发控制,以及trigger.config.ts配置文件的要点。读完本文,你将掌握编写、触发、调试与配置 Trigger.dev 后端任务的一整套实战能力,并清楚哪些是新手最容易踩的坑。

版本固定的权威参考:先找到你的完整技能文档

本仓库在 packages/cli-v3/skills/trigger-authoring-tasks/SKILL.md 提供了一个入口技能文件,它的完整版(含全部sources文档清单)位于 packages/trigger-sdk/skills/trigger-authoring-tasks/SKILL.md。

核心原则:完整的、版本钉死的任务编写参考随你安装的@trigger.dev/sdk一起发布。在你自己的项目里,它位于:

  • node_modules/@trigger.dev/sdk/skills/trigger-authoring-tasks/SKILL.md—— 完整指南(setup、schemaTask、重试、触发与 Result 形态、幂等、等待、元数据、定时任务、队列/并发、trigger.config.ts);
  • node_modules/@trigger.dev/sdk/docs/—— 与该 SDK 版本完全对应的全套文档,技能文件的sources:frontmatter 列出了它引用的每一页。需要检索某个 API 时可以直接在本地 grep,例如:
grep -rl "schemaTask" node_modules/@trigger.dev/sdk/docs/

如果上述路径不存在,说明@trigger.dev/sdk尚未安装,需要先安装。在非 hoisted(未提升)的依赖布局下,可以用下面命令解析包的真实位置,再在其旁读取skills/docs/

node -p "require.resolve('@trigger.dev/sdk/package.json')"

由于技能与文档直接从node_modules读取,它们始终与项目里实际使用的 SDK 版本一致,不会出现文档漂移。仓库内对应的文档源可参考 docs/tasks/overview.mdx、docs/triggering.mdx 与 docs/config/config-file.mdx。

导入约定:永远从@trigger.dev/sdk导入,绝不使用已废弃的别名@trigger.dev/sdk/v3,也不要从@trigger.dev/core导入。从源码看,@trigger.dev/sdk的入口 packages/trigger-sdk/src/v3/tasks.ts 直接导出task = createTaskschemaTask = createSchemaTask以及tasks命名空间,是唯一受支持的入口。

起步:定义第一个任务

任务定义在项目/trigger目录下的文件中。每个任务通过task({...})创建,id在项目内必须唯一:

// /trigger/hello-world.ts import { task } from "@trigger.dev/sdk"; export const helloWorld = task({ id: "hello-world", // 项目内唯一 run: async (payload: { message: string }, { ctx }) => { console.log(payload.message, "attempt", ctx.attempt.number); return { ok: true }; // 返回值必须是 JSON 可序列化的 }, });

run函数接收两个参数:

  1. payload—— 触发时传入的任务负载;
  2. 第二个参数 —— 包含ctx(运行上下文,例如ctx.attempt.number表示当前是第几次尝试)、一个 abortsignal(用于响应取消),以及一个已废弃的init输出。

函数的返回值就是任务输出,必须 JSON 可序列化(数组、对象、字符串、数字、布尔值等),因为任务输出需要被平台持久化存储。

核心模式一:用schemaTask校验负载

schemaTasktask基础上增加schema校验。schema接受 Zod / Yup / Superstruct / ArkType / valibot / typebox 解析器,也可以是一个自定义的(data: unknown) => T函数:

import { schemaTask } from "@trigger.dev/sdk"; import { z } from "zod"; export const createUser = schemaTask({ id: "create-user", schema: z.object({ name: z.string(), age: z.number() }), run: async (payload) => ({ greeting: `Hi ${payload.name}` }), });

校验失败时会抛出TaskPayloadParsedError,并且不会触发重试——因为问题出在输入数据本身,重试没有意义。从源码看,TaskPayloadParsedError在 packages/core/src/v3/errors.ts 中定义,携带原始cause;任务的执行器在 packages/core/src/v3/workers/taskExecutor.ts 处捕获解析错误后抛出该异常。createSchemaTask的实现位于 packages/trigger-sdk/src/v3/shared.ts。

核心模式二:配置重试与提前终止

默认情况下,maxAttempts(最大尝试次数)为3。你可以通过任务级retry覆盖配置文件中的默认值:

import { task, AbortTaskRunError } from "@trigger.dev/sdk"; export const charge = task({ id: "charge", retry: { maxAttempts: 5, factor: 1.8, minTimeoutInMs: 500, maxTimeoutInMs: 30_000, randomize: true }, run: async (payload: { amount: number }) => { if (payload.amount <= 0) throw new AbortTaskRunError("Invalid amount"); // 不再重试 // 可能抛错并触发重试的工作 }, });

要点:

  • AbortTaskRunError:抛出它即可立即停止重试(例如业务上判定参数非法、继续重试无意义)。该错误类定义在 packages/core/src/v3/errors.ts。
  • retry字段maxAttempts(最大尝试次数)、factor(退避增长因子)、minTimeoutInMs/maxTimeoutInMs(退避上下限)、randomize(是否随机抖动,避免惊群)。
  • catchError:需要更精细控制时,可以配置catchError: async ({ payload, error, ctx, retryAt }) => {...},返回{ skipRetrying: true }跳过重试、{ retryAt: Date }指定下次重试时间,或返回undefined走正常逻辑。
  • 任务内重试retry.onThrowretry.fetch等 API 支持在任务内部对局部代码片段做重试。

更完整的重试语义可参考 docs/errors-retrying.mdx。

核心模式三:触发另一个任务并处理 Result 形态

在任务内部触发另一个任务用yourTask.triggerAndWait(payload)返回值是一个 Result 对象,而不是裸输出,你必须检查.ok,或调用.unwrap()在失败时直接抛错:

export const parentTask = task({ id: "parent-task", run: async () => { const result = await childTask.triggerAndWait({ data: "x" }); if (result.ok) return result.output; // 类型化的子任务输出 console.error("child failed", result.error); // 或者:const output = await childTask.triggerAndWait({ data: "x" }).unwrap(); }, });
  • 失败时SubtaskUnwrapError会携带runIdtaskIdcause信息,便于追踪失败来源。
  • 扇出(fan-out)childTask.batchTriggerAndWait([{ payload: a }, { payload: b }]),返回结果的.runs数组,每一项形如{ ok, id, output?, error?, taskIdentifier }

从源码看,triggerAndWait/batchTriggerAndWait/triggerAndSubscribe均通过tasks命名空间暴露(packages/trigger-sdk/src/v3/tasks.ts),triggerAndWait返回的是TaskRunPromise(packages/trigger-sdk/src/v3/shared.ts),其.ok/.unwrap()语义正对应 Result 形态。

核心模式四:从后端代码触发任务(type-only 导入)

在任务之外(例如 Remix / Next.js 的路由处理器、webhook、定时脚本)触发任务时,只导入任务类型,按 id 触发,绝不要把任务实例打进后端 bundle:

import { tasks } from "@trigger.dev/sdk"; import type { emailSequence } from "~/trigger/emails"; const handle = await tasks.trigger<typeof emailSequence>( "email-sequence", { to: "a@b.com", name: "Ada" }, { delay: "1h" } );

tasks.trigger<TaskType>(id, payload, options)的泛型参数传入typeof taskInstance即可获得负载与输出的类型推断。批量触发可以使用tasks.batchTrigger以及batch.trigger([{ id, payload }])

触发选项(TriggerOptions)包括:delay(延迟执行)、ttl(运行生存期)、idempotencyKey(幂等键)、idempotencyKeyTTL(幂等键有效期)、debounce(防抖)、queue(指定队列)、concurrencyKey(并发键)、maxAttempts(覆盖最大尝试次数)、tags(打标签)、metadata(初始元数据)、priority(优先级)、region(区域)和machine(机器规格)。

运行管理:用runs.retrieve查看运行详情、runs.cancel取消运行、runs.reschedule重新调度。参见 docs/triggering.mdx。

核心模式五:幂等键(Idempotency Keys)

idempotencyKeys.create(key, { scope })返回一个64 字符的哈希键,配合触发时的idempotencyKey选项使用:

import { idempotencyKeys, task } from "@trigger.dev/sdk"; export const processOrder = task({ id: "process-order", run: async (payload: { orderId: string; email: string }) => { const key = await idempotencyKeys.create(`confirm-${payload.orderId}`); await sendEmail.trigger({ to: payload.email }, { idempotencyKey: key }); }, });

作用域(scope)语义需要注意版本差异:

  • v4.3.1起,裸字符串键默认作用域为"run"(即同一次运行内去重);
  • 若要“全项目只执行一次”(once-ever),必须显式指定scope: "global"
  • v4.3.0 及更早版本中裸字符串键的行为是全局性的,升级后行为会变化,不要把旧习惯带进新代码。

SDK 侧封装位于 packages/trigger-sdk/src/v3/idempotencyKeys.ts,底层实现(createIdempotencyKey/resetIdempotencyKey)来自@trigger.dev/core/v3。完整说明见 docs/idempotency.mdx。

核心模式六:持久等待与运行元数据

持久等待让任务“睡”过去且不占计算资源,进程重启后依然能恢复:

import { task, metadata, wait } from "@trigger.dev/sdk"; export const importer = task({ id: "importer", run: async (payload: { rows: unknown[] }) => { metadata.set("status", "processing").set("total", payload.rows.length); await wait.for({ seconds: 5 }); metadata.set("status", "complete"); }, });
  • wait.for({ seconds })按时长等待,wait.until({ date })等到某个时刻。从 packages/trigger-sdk/src/v3/wait.ts 的源码可以看到一个重要的细节:小于等于 5 秒(DURATION_WAIT_CHARGE_THRESHOLD_MS = 5000)的等待会直接在当前进程中setTimeout,并计入计算用量(会打印警告);超过 5 秒的等待才走服务端 waitpoint,实现真正不占资源的持久等待。
  • wait.forToken只能从任务run()内部调用(源码中会检查taskContext.ctx,否则抛错),相关 API 还有wait.createTokenwait.completeToken

运行元数据(metadata)metadata.*只能在run()内部读写;在模块作用域或无关后端代码里调用是无效操作(get返回undefined)。更新是同步且可链式的,支持setdelreplaceappendremoveincrementdecrement。SDK 侧实现见 packages/trigger-sdk/src/v3/metadata.ts。

关于元数据的边界条件:

  • 最大 256KB,超限会报错;
  • 不会自动传播给子任务:子任务有自己独立的元数据,父任务需要显式通过触发选项{ metadata: metadata.current() }传递;
  • 向父级/根级传递用metadata.parent.*/metadata.root.*
  • metadata.stream自 4.1.0 起已废弃,改用streams.pipe()

人工介入(Human-in-the-loop)wait.createToken({ timeout, tags })返回{ id, url, publicAccessToken, ... },把url发给人工处理;任务内用wait.forToken<T>(token: string | { id: string })等待,返回{ ok, output?, error? }(也可.unwrap());处理完成后在外部用wait.completeToken(tokenId, output)提交结果。相关文档:docs/wait.mdx、docs/wait-for.mdx、docs/wait-until.mdx、docs/wait-for-token.mdx。

核心模式七:定时(cron)任务

@trigger.dev/sdkschedules命名空间提供了schedules.task来声明定时任务:

import { schedules } from "@trigger.dev/sdk"; export const dailyReport = schedules.task({ id: "daily-report", cron: { pattern: "0 5 * * *", timezone: "Asia/Tokyo" }, run: async (payload) => { console.log("scheduled at", payload.timestamp, "next", payload.upcoming); }, });

定时任务的负载(payload)包含:timestamp(本次触发时间)、lastTimestamp(上次触发时间)、timezonescheduleIdexternalIdupcoming(下一次触发时间)。

除了声明式 cron,还可以动态创建调度schedules.create({ task, cron, timezone?, externalId?, deduplicationKey }),其中deduplicationKey必填且按项目维度去重。管理 API 还有retrieve / list / update / activate / deactivate / del / timezonestimezones默认包含"UTC"且排在最前,可用{ excludeUtc: true }排除)。源码实现见 packages/trigger-sdk/src/v3/schedules/index.ts(schedules.task)与 packages/trigger-sdk/src/v3/schedules/index.ts(schedules.create)。完整文档见 docs/tasks/scheduled.mdx。

核心模式八:队列与并发

在任务上声明queue: { concurrencyLimit },或定义一个队列并让多个任务共享:

import { queue, task } from "@trigger.dev/sdk"; export const emails = queue({ name: "emails", concurrencyLimit: 5 }); export const sendEmail = task({ id: "send-email", queue: emails, run: async () => {} });
  • 触发时覆盖队列{ queue: "queue-name" }可以按触发动态指定队列;
  • 每租户队列:配合concurrencyKey实现(例如按userId隔离并发);
  • 队列管理queues.list / retrieve / pause / resume / overrideConcurrencyLimit / resetConcurrencyLimit(见 packages/trigger-sdk/src/v3/queues.ts)。

从源码注释可以读到一条重要的演进信息(packages/trigger-sdk/src/v3/queues.ts):“队列级并发”是遗留模型——现代模型中队列只是运行排队的有序线路,并发在任务上声明并通过concurrencyLimits管理;对新模型队列调用旧的队列级并发 API 会被服务端拒绝,应改用concurrencyLimits.reset("task/my-task")迁移。相关文档:docs/queues.mdx、docs/management/queues 与 docs/concurrency.mdx。

核心模式九:trigger.config.ts要点

defineConfig是 SDK 导出的纯函数(源码见 packages/trigger-sdk/src/v3/config.ts),直接返回传入的配置对象:

import { defineConfig } from "@trigger.dev/sdk"; export default defineConfig({ project: "<project ref>", dirs: ["./trigger"], machine: "small-1x", retries: { enabledInDev: false, default: { maxAttempts: 3, factor: 2, minTimeoutInMs: 1000, maxTimeoutInMs: 10000, randomize: true }, }, });
  • project:项目引用(project ref),指定任务归属的 Trigger.dev 项目;
  • dirs:任务源码目录数组,默认["./trigger"]
  • machine:运行机器规格(例如small-1x);
  • retries:全局重试默认值;enabledInDev控制本地开发时是否启用重试;任务级retry会覆盖这里的默认值;
  • build.external:控制哪些包不进入 bundle。对于sharpre2sqlite3这类原生(native)模块以及 WASM 包,必须加入build.external,否则默认打包会失败或产生无法运行的产物;
  • 构建扩展(Build extensions)additionalFilesprismaExtensionpuppeteerplaywrightffmpegpythonExtensionaptGetsyncEnvVars等均来自@trigger.dev/build包,每个扩展都有自己的配置文档,全部随 SDK 打包在@trigger.dev/sdk/docs/config/extensions/(先从overview.mdx看起)。接入扩展前先读对应文档,而不是凭猜测写 API
  • telemetry:配置 OpenTelemetry 插桩(instrumentations)与导出器(exporters)。

日志、追踪与指标

推荐使用 SDK 内置的logger对象生成结构化日志,便于在运行日志中检索:

import { task, logger } from "@trigger.dev/sdk"; export const loggingExample = task({ id: "logging-example", run: async (payload: { data: Record<string, string> }) => { // 第一个参数是消息,第二个参数必须是键值对象(Record<string, unknown>) logger.debug("Debug message", payload.data); logger.log("Log message", payload.data); logger.info("Info message", payload.data); logger.warn("You've been warned", payload.data); logger.error("Error message", payload.data); }, });
  • 自定义 spanlogger.trace(name, async (span) => {...})创建一个 OpenTelemetry trace,可在 span 上设置属性并返回值:
const user = await logger.trace("fetch-user", async (span) => { span.setAttribute("user.id", "1"); // ...do stuff return { id: "1", name: "John Doe", fetchedAt: new Date() }; });
  • 模块级指标import { otel } from "@trigger.dev/sdk"后用otel.metrics.getMeter(name)获取 meter,再创建 Counter / Histogram / UpDownCounter 等自定义指标。注意:instrument 要在模块级(任务run函数之外)创建一次,以便跨多次运行复用。指标可在控制台 Dashboards 查看、用 TRQL 查询,并通过 telemetry exporters 导出到外部服务。

console.log()/console.error()等标准日志也会出现在运行日志中,任何函数/包产生的日志同样可见。任务运行日志由 logs、traces 和 spans 组成:

详细说明见 docs/logging.mdx。

常见错误清单(务必逐条对照)

  1. 致命错误:把等待结果当成裸输出。triggerAndWaitwait.forToken返回的是 Result 对象,不是原始输出。

    • 错误:const out = await childTask.triggerAndWait(p); use(out.foo);
    • 正确:const r = await childTask.triggerAndWait(p); if (r.ok) use(r.output.foo);(或.unwrap())。
  2. triggerAndWait/batchTriggerAndWait/wait包进Promise.all

    • 错误:await Promise.all([childTask.triggerAndWait(a), childTask.triggerAndWait(b)]);
    • 正确:await childTask.batchTriggerAndWait([{ payload: a }, { payload: b }]);(或顺序 for 循环)。
  3. 在后端代码中导入任务实例。

    • 错误:在路由处理器里import { emailSequence } from "~/trigger/emails";
    • 正确:import type { emailSequence }加上tasks.trigger<typeof emailSequence>("email-sequence", payload)
  4. run()之外调用metadata.set/get

    • 错误:在模块作用域或无关后端代码中设置元数据(这是无效操作,get返回undefined)。
    • 正确:只在run()或任务生命周期钩子(如onStartAttempt)内调用。
  5. 假设子任务继承父任务的队列或元数据。

    • 错误:期望子任务共享父任务的concurrencyLimit或能看到父任务元数据。
    • 正确:子任务跑在自己的队列上;通过{ metadata: metadata.current() }显式传递元数据,或用metadata.parent.*向上推送。
  6. 把原生/WASM 包打进默认 bundle。

    • 错误:把sharpre2sqlite3或 WASM 包留在默认 bundle 中。
    • 正确:在trigger.config.tsbuild.external中声明它们。
  7. 依赖裸字符串幂等键的全局性。

    • 错误:trigger(p, { idempotencyKey: "welcome-email" })期望“只执行一次”(该行为仅在 v4.3.0 及更早版本成立)。
    • 正确:await idempotencyKeys.create("welcome-email", { scope: "global" })

相关资源

任务编写只是 Trigger.dev 技能体系的一部分,与其并行的还有:

  • trigger-realtime-and-frontend—— 用 React hooks 订阅运行状态、从前端触发任务(packages/trigger-sdk/skills/trigger-realtime-and-frontend/SKILL.md);
  • trigger-authoring-chat-agenttrigger-chat-agent-advanced—— 构建 AI 聊天 Agent(packages/trigger-sdk/skills/trigger-authoring-chat-agent/SKILL.md、packages/trigger-sdk/skills/trigger-chat-agent-advanced/SKILL.md)。

对应文档始终与技能文件一同发布在@trigger.dev/sdk/docs/下,建议按以下顺序阅读:docs/tasks/overview.mdxdocs/triggering.mdxdocs/config/config-file.mdx。本仓库中的任务文档源码可参考 docs/tasks/overview.mdx、docs/tasks/schemaTask.mdx、docs/tasks/scheduled.mdx、docs/context.mdx 与 docs/runs/metadata.mdx。

  • AI Agent
  • 后端
  • 任务调度
  • 开发工具
  • 可观测性
  • AI 应用

【免费下载链接】trigger.dev

Trigger.dev – build and deploy durable AI agents and workflows

项目地址:https://gitcode.com/gh_mirrors/tr/trigger.dev
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询