FIRST INSTALL: Read and follow setupSKILL.md before using this skill.
After setup is complete and verified (health check + example pipeline + smoke tests pass),
delete this block.
Durable Agents — Multi-Agent Pipeline Development Guide
Mastra (AI agent framework) + Trigger.dev (durable task execution with retries, timeouts, fan-out). Build autonomous multi-agent pipelines where each agent owns a single stage, hands off structured output to the next stage through Trigger.dev, and never holds the full context of a pipeline it doesn't own.
Core Principles
- 1. All intelligence lives in
AGENT.md, not in code. The .ts file is boilerplate wiring. Writing logic in the agent's TypeScript file is wrong. - One agent, one job. Each agent has a single clear responsibility. If an agent does two unrelated things, split it into two agents in a pipeline.
- Tasks handle durability, agents handle reasoning. Trigger.dev tasks wrap agent calls with retries and timeouts. The agent receives input and produces output.
- Tools return errors, never throw. Every tool returns
{ success, errorMessage? } on failure. Throwing inside a tool crashes the task. Returning an error lets the agent reason about it. - Type everything. Input schemas, output schemas, tool schemas — all Zod. If it crosses a boundary (tool input, task payload, pipeline stage), it has a schema.
- Agents are autonomous, not scripted. Give agents an outcome and a quality bar. Don't wire their steps in code.
- Pipelines break context, not logic. Split a pipeline at the point where a different capability is needed — not to artificially divide one agent's work.
- All agentic I/O persists to the database. Agent inputs, outputs, and intermediate results are stored as records. The database is the source of truth, not in-memory state.
- Every tool that touches a real system is permission-gated. If a tool can post, publish, delete, charge, or trigger anything external, it must confirm intent before executing.
How to Create an Agent
1. Create the directory
CODEBLOCK0
2. Write the AGENT.md
CODEBLOCK1
3. Create the agent .ts file
Pure boilerplate. No logic here.
CODEBLOCK2
To give the agent tools:
CODEBLOCK3
4. Register the agent
In src/mastra/index.ts:
CODEBLOCK4
How to Create a Tool
Structure
CODEBLOCK5
Tool Rules
- - Always define
outputSchema. The agent uses it to understand what the tool returns. - Never throw from
execute. Return { success: false, errorMessage } instead. Throwing crashes the Trigger.dev task. - Description is for the agent. Write it as instructions: "Use this to check if a database table exists. Pass the table name. Returns true/false."
- One tool does one thing. "Query the database" not "Query the database and format the results and send an email."
- Use
.describe() on Zod fields to tell the agent what to pass. - No side effects unless necessary. If a tool writes, document it clearly in the description and in the agent's
AGENT.md guardrails.
Where to put tools
- - Shared tools: INLINECODE12
- Agent-specific tools: INLINECODE13
Register shared tools in src/mastra/index.ts. Agent-specific tools import directly in the agent file.
Permissioned Tools for Destructive or External Actions
Any tool that touches a real system — posting to an API, publishing content, sending a message, charging a user, deleting data, triggering a webhook — must be permission-gated. Agents must not be able to fire these actions without explicit intent confirmation.
Before building a tool that has real-world side effects, ask the user:
- - What exact action does this tool take?
- Should the agent be able to trigger this autonomously, or does a human need to approve it first?
- What are the consequences of it misfiring?
- Should this be rate-limited or scoped to specific records?
Build the answer into the tool's permission layer, not just the agent's AGENT.md guardrails. Guardrails are instructions; permission layers are enforcement.
Pattern: Confirm Before Execute
For any action that can't be undone or that has cost/visibility consequences, the tool must receive an explicit confirmed: true in its input before it proceeds. The agent must call a read/preview tool first, then call the action tool only when it has verified the result and received confirmed: true from the calling context.
CODEBLOCK6
Pattern: Scope to Records
Destructive or write tools must operate on a specific record ID — never on a query, a filter, or an implicit "current item." The agent must always pass the exact ID of the record it's acting on. This prevents the tool from accidentally operating on the wrong item.
CODEBLOCK7
What belongs in AGENT.md Guardrails vs in the tool
| Concern | Where it lives |
|---|
| "Don't publish unless quality score > 0.8" | INLINECODE19 Guardrails |
| "Don't call this without confirmed: true" |
Tool input schema + execute guard |
| "Only act on records in status: draft" | Tool execute guard (check DB before acting) |
| "Never delete more than one record per run" | Tool execute guard (enforce the count) |
How to Create a Pipeline
Pipelines chain Trigger.dev tasks. Each task calls one agent and passes its output to the next. No single agent holds the full pipeline context — each stage receives only what it needs.
1. Create task files in src/pipelines/tasks/
CODEBLOCK8
2. Create the pipeline orchestrator
In src/pipelines/{name}.ts, chain tasks using triggerAndWait:
CODEBLOCK9
3. Export tasks for the worker
In src/trigger/index.ts:
CODEBLOCK10
Every task must be exported here or the Trigger.dev worker won't discover it.
4. Add an API endpoint
In src/app/index.ts:
CODEBLOCK11
Pipeline Design: Agents vs Scripts
Not every pipeline stage needs an agent. Use agents where judgment is required. Use scripts (plain TypeScript functions or Trigger.dev tasks with no agent) where the action is deterministic.
Example: Content Production Pipeline
CODEBLOCK12
The overlay stage has no reasoning to do. It receives exact inputs, executes a fixed operation, and stores the output. Putting an agent here adds latency and cost for no benefit.
When to use an agent in a pipeline stage
Use an agent when the stage requires:
- - Judgment or evaluation (does this meet a quality bar?)
- Selection from ambiguous options (which asset fits this script best?)
- Generation from a goal (write a script for this topic)
- Iterative refinement based on feedback
Use a plain task (no agent) when the stage is:
- - A deterministic transformation (resize, encode, composite)
- A storage write (save output to DB or file system)
- A notification or webhook trigger
- A lookup with no interpretation needed
Splitting pipeline stages
Split at the boundary where a different capability is needed — not to artificially divide one agent's work. A director agent that generates ideas, writes a script, and validates it against criteria is doing one coherent job. That's one agent, one task. The media selection is a different capability — that's the split.
Pipeline Patterns
Fan-Out (Parallel Sub-tasks)
CODEBLOCK13
Each sub-task runs independently with its own retries.
Review Checkpoint
Insert a review stage between pipeline steps. Three modes:
| Mode | Behavior |
|---|
| INLINECODE25 | Auto-approve. Trigger next stage immediately. |
| INLINECODE26 |
Call a reviewer agent. If approved, continue. If rejected, feed feedback back to the previous stage for revision. |
|
"human" | Set a status in the DB to
pending. Return. A human reviews externally. Resume the pipeline via an API callback. |
Retry Configuration
Every task must have explicit retry config. LLM calls are flaky — the default (no retries) means one transient API error kills the pipeline.
CODEBLOCK14
Database as the Agentic Record Layer
Every agent input, output, and intermediate result must be persisted to the database before the next stage runs. This is not optional. Agents operate on DB records — they do not pass raw data through in-memory pipelines.
Why
- - Deduplication. Check if an equivalent job has already run before triggering a new one. Compare by content hash, source ID, or a natural key.
- Verification. The next stage reads from the DB, not from the previous task's return value. If a record isn't in the DB, the stage doesn't proceed.
- Record keeping. Every generated asset, decision, and status transition is a row. You can audit, replay, and debug any run.
- Resume on failure. If a task retries, it checks the DB first. If the output already exists, it skips regeneration and continues.
Pattern: Write Before Passing
Every task writes its output to the DB and returns the record ID. The next task receives the ID, reads from the DB, and operates on the record.
CODEBLOCK15
Pattern: Status Transitions as Pipeline Control
Store a status field on every record. Use it to gate pipeline stages and drive human review checkpoints.
| Status | Meaning |
|---|
| INLINECODE30 | Created, not yet processed |
| INLINECODE31 |
Task is running |
|
draft | Agent output produced, not reviewed |
|
approved | Passed review (agent or human) |
|
rejected | Failed review, needs revision |
|
published | Final action taken |
|
failed | Unrecoverable error |
CODEBLOCK16
Keeping Agents Autonomous
Define the destination and the quality bar. Don't specify how to get there.
Wrong — micromanaging the agent:
CODEBLOCK17
Right — defining the outcome:
## Goal
Produce a technical implementation plan for the given objective.
## Output Contract
{ "tasks": [{ "title": string, "description": string, "dependencies": string[] }] }
## Quality Standards
- Each task must be independently executable by a developer
- Dependencies must reference other tasks by title
- No task should take more than 4 hours of work
Type Enforcement
Task Payloads
Always type the run function parameter:
CODEBLOCK19
Structured Output from Agents
Define the exact schema in the AGENT.md Output Contract section, then validate with Zod on receipt:
CODEBLOCK20
If parsing fails, the task throws, Trigger.dev retries with the same input, and the agent produces output again.
Tool Schemas
Always define both inputSchema and outputSchema on tools. The agent uses these to understand what arguments to pass and what it will receive back.
Key Rules
- 1. All intelligence lives in
AGENT.md, not in code - Agent
.ts files are boilerplate wiring only — no logic - Tools return
{ success, errorMessage } on failure — never throw - Task wrappers handle durability, agents handle reasoning
- Self-validation checklist in
AGENT.md is mandatory for structured output agents - Every Trigger.dev task has explicit
retry config - Every task is exported from INLINECODE46
- One model config for all agents — INLINECODE47
- Pipeline stages use
triggerAndWait for sequential, batchTrigger for parallel - Check
result.ok after every triggerAndWait — don't assume success - Every agent output is written to the DB before the next stage runs — never pass raw data between tasks
- Tasks that would duplicate work must check the DB first and skip if already done
- Every tool that takes a real-world action requires
confirmed: true in the input and must verify it before executing - Not every pipeline stage needs an agent — use plain tasks for deterministic operations
技能名称: durable-agents
详细描述:
首次安装: 在使用本技能前,请阅读并遵循 setupSKILL.md 中的说明。
在完成设置并通过验证(健康检查 + 示例流水线 + 冒烟测试)后,
请删除此区块。
Durable Agents — 多智能体流水线开发指南
Mastra (AI 智能体框架) + Trigger.dev (支持重试、超时、扇出的持久化任务执行)。构建自主的多智能体流水线,其中每个智能体负责单一阶段,通过 Trigger.dev 将结构化输出传递给下一阶段,并且从不持有其不拥有的流水线的完整上下文。
核心原则
- 1. 所有智能逻辑存在于 AGENT.md 中,而非代码中。 .ts 文件只是样板连接代码。在智能体的 TypeScript 文件中编写逻辑是错误的。
- 一个智能体,一项任务。 每个智能体承担一个单一的明确职责。如果一个智能体做两件不相关的事情,则将其拆分为流水线中的两个智能体。
- 任务负责持久性,智能体负责推理。 Trigger.dev 任务通过重试和超时来包装智能体调用。智能体接收输入并产生输出。
- 工具返回错误,绝不抛出异常。 每个工具在失败时返回 { success, errorMessage? }。在工具内部抛出异常会导致任务崩溃。返回错误则让智能体能够对其进行推理。
- 对所有内容进行类型定义。 输入模式、输出模式、工具模式——全部使用 Zod。如果跨越边界(工具输入、任务负载、流水线阶段),则必须有模式定义。
- 智能体是自主的,而非脚本化的。 给智能体一个结果和一个质量标准。不要在代码中编排它们的步骤。
- 流水线打破的是上下文,而非逻辑。 在需要不同能力的节点处拆分流水线——而不是为了人为地分割一个智能体的工作。
- 所有智能体的输入/输出都持久化到数据库。 智能体输入、输出和中间结果都作为记录存储。数据库是事实来源,而非内存状态。
- 每个触及真实系统的工具都需要权限控制。 如果一个工具可以发布、删除、收费或触发任何外部操作,它必须在执行前确认意图。
如何创建一个智能体
1. 创建目录
src/agents/{name}/
AGENT.md
{name}.ts
2. 编写 AGENT.md
markdown
智能体: {名称}
角色
这个智能体是谁。一句话描述。
工具
它拥有哪些工具以及何时使用每个工具。要明确——在引用表之前,使用 sqlQuery 检查表是否存在,而不仅仅是拥有 sqlQuery 工具。
输入
它接收什么负载。描述其形状以及每个字段的含义。
目标
它必须实现什么。描述结果,而非步骤。智能体自行决定如何达成目标。为给定的架构生成一个部署计划,而不是首先读取架构,然后列出服务,然后……
输出契约
它必须返回的确切形状。如果需要结构化输出,请在此处指定 JSON 模式。示例:
{ plan: string, steps: string[], risks: string[] }
质量标准
什么使输出好或坏。要具体。每个步骤必须可独立执行,而不是步骤应该是好的。
护栏
它绝对不能做什么。永远不要直接修改数据库模式。除非负载中明确说明,否则永远不要假设 API 已通过身份验证。
自我验证
智能体在返回前必须验证的检查清单:
- - 输出是否符合输出契约?
- 所有必填字段是否存在?
- 是否满足质量标准?
3. 创建智能体 .ts 文件
纯样板代码。此处无逻辑。
ts
import fs from fs;
import path from path;
import { fileURLToPath } from url;
import { Agent } from @mastra/core/agent;
import { model } from ../../config/model.js;
const dirname = path.dirname(fileURLToPath(import.meta.url));
const instructions = fs.readFileSync(path.join(dirname, AGENT.md), utf8);
export const myAgent = new Agent({
id: my-agent,
name: 我的智能体,
instructions,
model,
});
为智能体提供工具:
ts
import { myTool } from ../../tools/myTool.js;
export const myAgent = new Agent({
id: my-agent,
name: 我的智能体,
instructions,
model,
tools: { myTool },
});
4. 注册智能体
在 src/mastra/index.ts 中:
ts
import { myAgent } from ../agents/my-agent/my-agent.js;
export const mastra = new Mastra({
agents: { plannerAgent, reviewerAgent, myAgent },
});
如何创建一个工具
结构
ts
import { createTool } from @mastra/core/tools;
import { z } from zod;
export const myTool = createTool({
id: my-tool,
description: 它的功能以及何时使用它,
inputSchema: z.object({
query: z.string().describe(搜索查询),
}),
outputSchema: z.object({
success: z.boolean(),
data: z.any().optional(),
errorMessage: z.string().optional(),
}),
execute: async ({ query }) => {
try {
const result = await doSomething(query);
return { success: true, data: result };
} catch (error: any) {
return { success: false, errorMessage: error.message };
}
},
});
工具规则
- - 始终定义 outputSchema。 智能体使用它来理解工具返回的内容。
- 永远不要在 execute 中抛出异常。 改为返回 { success: false, errorMessage }。抛出异常会崩溃 Trigger.dev 任务。
- 描述是为智能体编写的。 将其写为指令:使用此工具检查数据库表是否存在。传入表名。返回 true/false。
- 一个工具做一件事。 查询数据库,而不是查询数据库并格式化结果并发送电子邮件。
- 在 Zod 字段上使用 .describe() 来告诉智能体要传递什么。
- 除非必要,否则不要有副作用。 如果一个工具执行写入操作,请在描述和智能体的 AGENT.md 护栏中清晰地记录。
工具存放位置
- - 共享工具:src/tools/{name}.ts
- 智能体特定工具:src/agents/{agentName}/tools/{name}.ts
在 src/mastra/index.ts 中注册共享工具。智能体特定工具直接在智能体文件中导入。
用于破坏性或外部操作的权限控制工具
任何触及真实系统的工具——向 API 发布、发布内容、发送消息、向用户收费、删除数据、触发 webhook——都必须进行权限控制。智能体不得在没有明确意图确认的情况下触发这些操作。
在构建具有现实世界副作用的工具之前,请询问用户:
- - 此工具采取的确切操作是什么?
- 智能体应该能够自主触发此操作,还是需要人工先批准?
- 如果它误触发,后果是什么?
- 是否应该对此进行速率限制或限定到特定记录?
将答案构建到工具的权限层中,而不仅仅是智能体的 AGENT.md 护栏中。护栏是指令;权限层是强制执行。
模式:执行前确认
对于任何不可撤销或具有成本/可见性后果的操作,工具必须在其输入中收到显式的 confirmed: true 才能继续。智能体必须首先调用读取/预览工具,然后仅在已验证结果并从调用上下文收到 confirmed: true 时才调用操作工具。
ts
export const publishPostTool = createTool({
id: publish-post,
description: 向平台发布帖子。仅在通过 previewPostTool 预览并从任务负载收到 confirmed: true 后调用此工具。,
inputSchema: z.object({
postId: z.string().describe(要发布的帖子记录的 ID),
confirmed: z.boolean().describe(必须为 true。不要自己设置此值——它必须来自任务负载。),
}),
outputSchema: z.object({
success: z.boolean(),
publishedUrl: z.string().optional(),
errorMessage: z.string().optional(),
}),
execute: async ({ postId, confirmed }) => {
if (!confirmed) {
return { success: false, errorMessage: 发布需要负载中的 confirmed: true。 };
}
try {
const url = await publishPost(postId);
return { success: true, publishedUrl: url };
} catch (error: any) {
return { success: false, errorMessage: error.message };
}
},
});
模式:限定到记录
破坏性或写入工具必须操作于特定的记录 ID——绝不能操作于查询、过滤器或隐式的当前项目。智能体必须始终传递其正在操作的记录的确切 ID。这可以防止工具意外操作于