|
| 1 | +--- |
| 2 | +title: "Agent Skills" |
| 3 | +sidebarTitle: "Agent Skills" |
| 4 | +description: "Ship reusable capabilities (folders with SKILL.md + scripts) that a chat agent discovers and invokes on demand." |
| 5 | +--- |
| 6 | + |
| 7 | +Agent skills are reusable capabilities you ship as folders — a `SKILL.md` describing when and how to use them, plus optional scripts, references, and assets. The chat agent sees a short description of each skill in its system prompt, loads the full instructions on demand via a `loadSkill` tool, and invokes the bundled scripts via `bash` — all without you wiring anything up manually. |
| 8 | + |
| 9 | +Built on the [AI SDK cookbook pattern](https://ai-sdk.dev/cookbook/guides/agent-skills). Works with any provider (OpenAI, Anthropic, Gemini, etc.) — not tied to Anthropic's server-side skills. |
| 10 | + |
| 11 | +## Why skills? |
| 12 | + |
| 13 | +Compared to regular AI SDK tools: |
| 14 | + |
| 15 | +- **Tools** are typed functions you pre-declare. Great when you know up-front exactly what capability the agent needs. |
| 16 | +- **Skills** are folders the model discovers and reads on demand. Great when the capability is a bundle of instructions + helper scripts that would be awkward to encode as a single tool. |
| 17 | + |
| 18 | +PDFs are the canonical example: you don't want to ask the LLM to parse PDF bytes inline. You want it to `bash scripts/extract.py report.pdf` using a bundled `pdfplumber` wrapper. A skill ships the script, the instructions, and any reference notes together. |
| 19 | + |
| 20 | +Skills are also [dashboard-editable](/ai-chat/skills/overview) in Phase 2 — a platform team can tighten a skill's description or "when to use" text without a redeploy. Phase 1 (today) is SDK-only. |
| 21 | + |
| 22 | +## Trust model |
| 23 | + |
| 24 | +Skills are **developer-authored code**, not end-user-supplied. The same developer who writes the `chat.agent()` writes the skill bundle. The trust boundary is identical to any `tool.execute` handler the developer writes — scripts run directly in the Trigger.dev worker container, no sandboxing required. |
| 25 | + |
| 26 | +This makes skills different from the Claude Code / end-user model where arbitrary user-provided skills need isolation. Don't accept skill paths from untrusted input. |
| 27 | + |
| 28 | +## Skill folder layout |
| 29 | + |
| 30 | +A skill is a directory under your project (conventionally `trigger/skills/{id}/`): |
| 31 | + |
| 32 | +``` |
| 33 | +trigger/skills/time-utils/ |
| 34 | +├── SKILL.md # Required — frontmatter + instructions |
| 35 | +├── scripts/ |
| 36 | +│ ├── now.sh |
| 37 | +│ └── add.sh |
| 38 | +├── references/ |
| 39 | +│ └── timezones.txt |
| 40 | +└── assets/ # Optional — templates, data files, etc. |
| 41 | +``` |
| 42 | + |
| 43 | +### SKILL.md |
| 44 | + |
| 45 | +Frontmatter is YAML-subset — only `name` and `description` are required: |
| 46 | + |
| 47 | +```md |
| 48 | +--- |
| 49 | +name: time-utils |
| 50 | +description: Compute and format dates/times in arbitrary timezones. Use when the user asks "what time is it", timezone conversions, or date math. |
| 51 | +--- |
| 52 | + |
| 53 | +# Time utilities |
| 54 | + |
| 55 | +## When to use |
| 56 | + |
| 57 | +- The user asks for the current time in a timezone |
| 58 | +- The user wants date math ("3 days from now") |
| 59 | + |
| 60 | +## Scripts |
| 61 | + |
| 62 | +### `scripts/now.sh [TZ]` |
| 63 | +Prints the current time in the given IANA timezone (default `UTC`). |
| 64 | + |
| 65 | +### `scripts/add.sh DAYS [TZ]` |
| 66 | +Prints a date `DAYS` days from now. |
| 67 | + |
| 68 | +## Tips |
| 69 | +- IANA timezone names only (`America/New_York`, not `EST`). |
| 70 | +- See `references/timezones.txt` for a cheat-sheet. |
| 71 | +``` |
| 72 | + |
| 73 | +The **description** is what the model sees in its system prompt — write it like you're explaining to the agent when to reach for the skill. |
| 74 | + |
| 75 | +The **body** is loaded on demand via the `loadSkill` tool when the agent decides to use the skill. Write it like documentation for the agent. |
| 76 | + |
| 77 | +## Defining and using a skill |
| 78 | + |
| 79 | +```ts trigger/chat.ts |
| 80 | +import { chat } from "@trigger.dev/sdk/ai"; |
| 81 | +import { skills } from "@trigger.dev/sdk"; |
| 82 | +import { streamText } from "ai"; |
| 83 | +import { openai } from "@ai-sdk/openai"; |
| 84 | + |
| 85 | +const timeUtilsSkill = skills.define({ |
| 86 | + id: "time-utils", |
| 87 | + path: "./skills/time-utils", |
| 88 | +}); |
| 89 | + |
| 90 | +export const agent = chat.agent({ |
| 91 | + id: "docs-chat", |
| 92 | + onChatStart: async () => { |
| 93 | + chat.skills.set([await timeUtilsSkill.local()]); |
| 94 | + }, |
| 95 | + run: async ({ messages, signal }) => { |
| 96 | + return streamText({ |
| 97 | + model: openai("gpt-4o"), |
| 98 | + messages, |
| 99 | + abortSignal: signal, |
| 100 | + ...chat.toStreamTextOptions(), |
| 101 | + }); |
| 102 | + }, |
| 103 | +}); |
| 104 | +``` |
| 105 | + |
| 106 | +`skills.define({ id, path })` does two things: |
| 107 | + |
| 108 | +1. Registers the skill with the Trigger.dev build system so the CLI **automatically bundles the folder** into your deploy image at `/app/.trigger/skills/{id}/`. No `trigger.config.ts` changes, no build extension — it just works. |
| 109 | +2. Returns a `SkillHandle` you use at runtime. |
| 110 | + |
| 111 | +`skill.local()` reads the bundled `SKILL.md` from disk and returns a `ResolvedSkill` with the parsed frontmatter + body + on-disk path. |
| 112 | + |
| 113 | +`chat.skills.set([...])` stores the resolved skills for the current run. `chat.toStreamTextOptions()` spreads them into `streamText` automatically: |
| 114 | + |
| 115 | +- The frontmatter `description` lands in the system prompt under "Available skills:". |
| 116 | +- Three tools are added: `loadSkill`, `readFile`, `bash` — scoped per skill. |
| 117 | + |
| 118 | +## What gets auto-injected |
| 119 | + |
| 120 | +When you spread `chat.toStreamTextOptions()` with skills set, the AI SDK call receives three tools: |
| 121 | + |
| 122 | +### `loadSkill({ name })` |
| 123 | + |
| 124 | +Returns the full `SKILL.md` body for the named skill. The model calls this first when it decides a skill is relevant, to load the full instructions. |
| 125 | + |
| 126 | +### `readFile({ skill, path })` |
| 127 | + |
| 128 | +Reads a file inside the skill's bundled folder. Paths are relative to the skill's root and are rejected if they attempt to escape via `..` or absolute paths. Output is capped at 1 MB per call. |
| 129 | + |
| 130 | +Use for reference files and templates that the model should read literally: |
| 131 | + |
| 132 | +``` |
| 133 | +readFile({ skill: "time-utils", path: "references/timezones.txt" }) |
| 134 | +``` |
| 135 | + |
| 136 | +### `bash({ skill, command })` |
| 137 | + |
| 138 | +Runs a bash command with `cwd` set to the skill's root. Stdout and stderr are captured and returned (each capped at 64 KB per call, with tail truncation). The turn's abort signal propagates — cancelling the run kills the child process. |
| 139 | + |
| 140 | +Use to invoke the skill's bundled scripts: |
| 141 | + |
| 142 | +``` |
| 143 | +bash({ skill: "time-utils", command: "bash scripts/now.sh America/Los_Angeles" }) |
| 144 | +``` |
| 145 | + |
| 146 | +Script runtime expectations are yours to manage. If your skill uses `extract.py`, your deploy image needs Python — add it via your build config the same way you would for any other task dependency. |
| 147 | + |
| 148 | +## How discovery works in the model |
| 149 | + |
| 150 | +The model sees a short preamble appended to your system prompt: |
| 151 | + |
| 152 | +``` |
| 153 | +Available skills (call `loadSkill` to read the full instructions before using one): |
| 154 | +- time-utils: Compute and format dates/times in arbitrary timezones... |
| 155 | +- pdf-processing: Extract text from PDFs, fill forms... |
| 156 | +``` |
| 157 | + |
| 158 | +When the user asks something that matches a description, the model calls `loadSkill({ name: "time-utils" })` to load the body, then follows the body's instructions — typically by calling `bash` or `readFile` on the bundled scripts. |
| 159 | + |
| 160 | +This is **progressive disclosure**: each skill costs ~100 tokens up front (its one-line description), and only the ones the model actually uses pay the full context cost. |
| 161 | + |
| 162 | +## Mixing skills with custom tools |
| 163 | + |
| 164 | +If you also define your own AI SDK tools, pass them through `chat.toStreamTextOptions()` so the merge is explicit: |
| 165 | + |
| 166 | +```ts |
| 167 | +return streamText({ |
| 168 | + model: openai("gpt-4o"), |
| 169 | + messages, |
| 170 | + abortSignal: signal, |
| 171 | + ...chat.toStreamTextOptions({ |
| 172 | + tools: { |
| 173 | + webFetch, // your tool |
| 174 | + deepResearch, // your tool |
| 175 | + }, |
| 176 | + }), |
| 177 | +}); |
| 178 | +``` |
| 179 | + |
| 180 | +Your tools win on name conflicts. (Pick names that don't collide with `loadSkill` / `readFile` / `bash` to keep things predictable.) |
| 181 | + |
| 182 | +## Bundling |
| 183 | + |
| 184 | +Bundling is **built-in to the CLI** — there's no extension to import. When you run `trigger deploy` or `trigger dev`: |
| 185 | + |
| 186 | +1. esbuild bundles your task code as usual. |
| 187 | +2. The CLI forks the indexer locally against the bundled output, collects every `skills.define({ path })` registration. |
| 188 | +3. Each skill's folder is copied to `{outputPath}/.trigger/skills/{id}/` via a recursive copy. |
| 189 | +4. The existing Dockerfile `COPY` picks up `.trigger/skills/` along with the rest of the bundle — no Dockerfile changes. |
| 190 | + |
| 191 | +If you're running `trigger dev`, the same layout appears in the local dev output directory, so `skill.local()` works the same way. |
| 192 | + |
| 193 | +## Path scoping rules |
| 194 | + |
| 195 | +- `skill.path` always resolves to `${process.cwd()}/.trigger/skills/{id}/` at runtime. Don't hardcode paths elsewhere. |
| 196 | +- `readFile` rejects `..` segments and absolute paths — the tool only exposes files inside the skill's own directory. |
| 197 | +- `bash` runs with `cwd` set to the skill's root. Inside the script, relative paths resolve against the skill directory. |
| 198 | +- Cross-skill access isn't provided — each skill is isolated by design. If two skills need to share data, either duplicate the shared file or consolidate the skills. |
| 199 | + |
| 200 | +## Limitations in Phase 1 |
| 201 | + |
| 202 | +- `skill.resolve()` (backend-managed overrides) is not available yet. It throws a "not available in Phase 1, use `.local()`" error. Phase 2 ships dashboard-editable `SKILL.md` text. |
| 203 | +- No per-skill metrics in the dashboard yet. |
| 204 | +- No Anthropic `/v1/skills` integration — use the portable path today; the Anthropic optimization comes in Phase 4. |
| 205 | + |
| 206 | +## Full example |
| 207 | + |
| 208 | +See `references/ai-chat/src/trigger/skills/time-utils/` in the Trigger.dev monorepo for a working skill that bundles two bash scripts and a reference cheat-sheet, wired into a `chat.agent` that answers timezone questions. |
| 209 | + |
| 210 | +## Related |
| 211 | + |
| 212 | +- [AI SDK cookbook — Agent Skills](https://ai-sdk.dev/cookbook/guides/agent-skills) — the userland pattern we build on |
| 213 | +- [Anthropic Agent Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) — Anthropic's codified version (server-side, optional future integration) |
0 commit comments