Claude
Skills
Sign in
Back

durable-agents

Included with Lifetime
$97 forever

Build autonomous multi-agent pipelines with Mastra (agents only) and Trigger.dev (all workflows and tasks). Use when creating AI agents, designing multi-stage pipelines, defining permissioned tools, structuring agent handoffs, storing agentic outputs to a database, or building durable task chains with retries and fan-out.

AI Agents

What this skill does


> **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.
2. **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.
3. **Tasks handle durability, agents handle reasoning.** Trigger.dev tasks wrap agent calls with retries and timeouts. The agent receives input and produces output.
4. **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.
5. **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.
6. **Agents are autonomous, not scripted.** Give agents an outcome and a quality bar. Don't wire their steps in code.
7. **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.
8. **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.
9. **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

```
src/agents/{name}/
  AGENT.md
  {name}.ts
```

### 2. Write the `AGENT.md`

```markdown
# AGENT: {Name}

## Role
Who this agent is. One sentence.

## Tools
What tools it has and when to use each one. Be explicit — "Use `sqlQuery` to
check if a table exists before referencing it" not just "Has sqlQuery tool."

## Inputs
What payload it receives. Describe the shape and what each field means.

## Goal
What it must achieve. Describe the outcome, not the steps. The agent decides
how to get there. "Produce a deployment plan for the given architecture" not
"First read the architecture, then list the services, then..."

## Output Contract
Exact shape it must return. If structured output is needed, specify the JSON
schema here. Example:
  { "plan": string, "steps": string[], "risks": string[] }

## Quality Standards
What makes output good vs bad. Be specific. "Each step must be independently
executable" not "Steps should be good."

## Guardrails
What it must NOT do. "Never modify database schema directly." "Never assume
the API is authenticated unless payload says so."

## Self-Validation
Checklist the agent must verify before returning:
- Does output match the Output Contract?
- Are all required fields present?
- Does it satisfy the Quality Standards?
```

### 3. Create the agent `.ts` file

Pure boilerplate. No logic here.

```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: "My Agent",
    instructions,
    model,
});
```

To give the agent tools:

```ts
import { myTool } from "../../tools/myTool.js";

export const myAgent = new Agent({
    id: "my-agent",
    name: "My Agent",
    instructions,
    model,
    tools: { myTool },
});
```

### 4. Register the agent

In `src/mastra/index.ts`:

```ts
import { myAgent } from "../agents/my-agent/my-agent.js";

export const mastra = new Mastra({
    agents: { plannerAgent, reviewerAgent, myAgent },
});
```

---

## How to Create a Tool

### Structure

```ts
import { createTool } from "@mastra/core/tools";
import { z } from "zod";

export const myTool = createTool({
    id: "my-tool",
    description: "What it does and WHEN to use it",
    inputSchema: z.object({
        query: z.string().describe("The search query"),
    }),
    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 };
        }
    },
});
```

### 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: `src/tools/{name}.ts`
- Agent-specific tools: `src/agents/{agentName}/tools/{name}.ts`

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.

```ts
export const publishPostTool = createTool({
    id: "publish-post",
    description: "Publishes a post to the platform. Only call this after previewing with `previewPostTool` and receiving confirmed: true from the task payload.",
    inputSchema: z.object({
        postId: z.string().describe("ID of the post record to publish"),
        confirmed: z.boolean().describe("Must be true. Do not set this yourself — it must come from the task payload."),
    }),
    outputSchema: z.object({
        success: z.boolean(),
        publishedUrl: z.string().optional(),
        errorMessage: z.string().optional(),
    }),
    execute: async ({ postId, confirmed }) => {
        if (!confirmed) {
            return { success: false, errorMessage: "Publish requires confirmed: true in payload."

Related in AI Agents