Skills
Give an agent instructions for specific tasks that it loads only when a task needs them.
A skill is a SKILL.md file with a name and a description, followed by instructions for one kind of task. The model sees every skill’s name and description in the system prompt and loads a skill’s instructions only when a task matches, so an agent can have many skills without filling its context.
Write a skill
Put each skill in its own folder as skills/<name>/SKILL.md:
---
name: commit-style
description: How to write commit messages in this project.
---
Write commit messages in the imperative mood and keep the subject under 50 characters.
The model decides when to use a skill from its description alone, so say which tasks it covers.
Load skills
Pi doesn’t discover skill files on its own. This extension lists them in a prompt section and loads them with a tool:
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { Type } from "@earendil-works/pi-ai";
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
import { defineExtension, defineTool, section } from "@earendil-works/pi-durable";
// One folder per skill, next to this file: skills/<name>/SKILL.md.
const root = join(import.meta.dirname, "skills");
const skills = new Map<string, { description: string; body: string }>();
for (const folder of await readdir(root, { withFileTypes: true })) {
if (!folder.isDirectory()) continue;
const file = await readFile(join(root, folder.name, "SKILL.md"), "utf8");
const { frontmatter, body } = parseFrontmatter<{ name?: string; description?: string }>(file);
skills.set(frontmatter.name ?? folder.name, { description: frontmatter.description ?? "", body });
}
const loadSkill = defineTool({
name: "skill",
description: "Load a skill's instructions by name.",
parameters: Type.Object({ name: Type.String() }),
// Reading a skill changes nothing, so running it twice is harmless.
replay: "safe",
execute: async ({ name }) => {
const skill = skills.get(name);
if (!skill) throw new Error(`No skill named ${name}. Skills: ${[...skills.keys()].join(", ")}.`);
return { content: [{ type: "text", text: skill.body }] };
},
});
export const Skills = defineExtension({
name: "skills",
tools: [loadSkill],
sections: [
// The model sees every skill's name and description. It loads a skill's instructions only when it needs them.
section("skills", () =>
[
"Skills hold instructions for specific tasks. Before you start a task that matches a skill's description, call the `skill` tool with its name and follow what it returns.",
...[...skills].map(([name, { description }]) => `- ${name}: ${description}`),
].join("\n"),
),
],
});
import { createRegistry } from "@earendil-works/pi-durable";
import { CodingTools } from "@earendil-works/pi-durable/tools";
import { pi } from "@rivet-dev/pi";
import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b";
import { setup } from "rivetkit";
import { Skills } from "./skills";
const extensions = createRegistry();
extensions.install(CodingTools);
extensions.install(Skills);
const agent = pi({
model: "anthropic/claude-opus-5-5",
registry: extensions,
sandbox: e2bProvider(),
});
export const registry = setup({ use: { agent } });
registry.start();
- The
skillssection lists every skill’s name and description. Like every section, it renders before each model call. - The
skilltool returns a skill’s instructions. It setsreplay: "safe", so a call cut off by a crash runs again. - Skills ship with your worker. They’re read when the worker starts, and a deploy updates them. They work with or without a sandbox, and the agent can’t change them.
- Skills can differ per agent. Look them up from your database in the section and the tool instead of from files, and every agent gets its own list.
Skills in the workspace
Skill files that are already in the workspace, such as .agents/skills/ in a repository cloned into the sandbox, need no tool. Add a section that says where they are, and the model reads a skill’s SKILL.md with Pi’s read tool before it starts the task.
Next: Extensions, how to bundle tools, sections, and hooks and choose them per conversation.