Four Ways to Change Pi
A decision procedure for choosing between context files, prompt templates, skills, and extensions, with four worked cases and one running example for the rest of this part.
You keep typing the same three lines before every code review. Then you notice the same file appearing in three repositories. Then someone asks for a /review-tests command and you write forty lines of TypeScript to expand a string.
Every one of those three moves was available. Pi’s documentation gives a one-line rule for choosing between them — quickstart.md says to start with the least powerful mechanism that meets your need — and then leaves the reader to guess what “least powerful” means at three in the afternoon.
This chapter turns it into a procedure: four questions and one diagram that a reader can come back to. Chapters 12 through 18 each take one branch of that procedure and go deep, carrying the same running example between them.
The mechanisms
quickstart.md tabulates the whole set, in the order it wants you to climb it:
| Need | Start with |
|---|---|
| Give Pi persistent instructions for a folder | AGENTS.md |
Reuse a prompt from the / menu |
Prompt template |
| Add task-specific instructions and supporting files | Skill |
| Add executable tools, commands, or event handlers | Extension |
| Build a custom terminal component | Terminal UI |
| Connect an unsupported model service | Custom provider |
| Install or distribute several resources | Pi package |
The first four are text — except the last of them, which is a TypeScript module running inside the Pi process with your operating-system permissions. Chapters 12 through 18 take those four in turn, in the order the procedure reaches them.
The last three rows are not higher rungs. They solve different problems rather than harder versions of the same one: a terminal component renders, a provider supplies models, and a package is a distribution format that can contain any of the four.
The procedure
Ask four questions in order. Stop at the first “yes”.
flowchart TD
N["A need you keep repeating"] --> Q1{"Needs executable behaviour?"}
Q1 -->|"yes"| EXT["Extension"]
Q1 -->|"no"| Q2{"Applies to every task here?"}
Q2 -->|"yes"| AG["AGENTS.md or context file"]
Q2 -->|"no"| Q3{"Longer than a screen, or needs files?"}
Q3 -->|"yes"| SK["Skill"]
Q3 -->|"no"| PT["Prompt template"]
1. Does the change need to run code, or observe or block something at runtime?
If it needs to intercept a tool call, react to a session event, add a slash command with behaviour, or change the system prompt programmatically, it is an extension. extensions.md is explicit that extensions are the mechanism for workflows needing “tools, commands, event handlers, model providers, session state, or terminal UI rather than instructions alone.”
2. Does the change apply on every request, for every task, in this folder?
If yes, it is a context file. configuration.md describes a context file as applying “whenever Pi runs in its directory or anywhere below it.” If the behaviour is conditional on something — a file type, a task shape, a trigger word — a context file is the wrong home, because a context file has no condition.
3. Is the content more than about a screen of text, or does it need files of its own?
If yes, it is a skill. skills.md says to use one “when a workflow needs more context than a prompt template but does not need a new executable integration point,” and skills can bundle scripts, references, and assets.
4. Otherwise it is a prompt template.
prompt-templates.md says to use one “when you want to reuse the same prompt without adding executable behavior or a larger set of supporting instructions.”
Why not the rung below
Stopping at the first “yes” only works if the rejections are forced rather than preferred. They are:
| Rejected | Because |
|---|---|
AGENTS.md, for a conditional need |
A context file has no condition. configuration.md says it applies “whenever Pi runs in its directory or anywhere below it”, so a rule that should fire only for reviews fires on every task in the folder. |
| Prompt template, for a long workflow | One prompt and no files. The checklist, the reference table, and the script have nowhere to live. |
| Skill, for a guard | skills.md reserves skills for a workflow that “does not need a new executable integration point”. A skill is instructions the model reads and follows; only pi.on("tool_call") runs before the call does. |
A package is not a fifth rung
packages.md describes a package as an ordinary directory or npm package that installs extensions, skills, prompt templates, and themes “as one unit,” with its own runtime dependencies. It answers “how do these travel together”, not “what should this be”.
The order of decisions is therefore: choose the mechanism first, then choose the distribution.
Signals and their answers
The four questions cover the general case. When you already recognise the situation, these are the answers — including the ones people get wrong in both directions.
| Signal in your situation | Mechanism | Why |
|---|---|---|
| “I need a new operation the model can call” | Extension | Only an extension adds a tool |
| “I need to block or rewrite a tool call” | Extension | Only an extension registers a tool_call handler |
“I need a / command that does something” |
Extension | pi.registerCommand() takes a handler |
| “It is 5 lines of text” | AGENTS.md |
Anything longer is probably worth its own file |
| “It depends on which file type I am working on” | Skill | The description is the routing mechanism |
Worked case 1: “always use the repo’s test command”
You want Pi to run pnpm test:unit rather than inventing a command.
Decision. Question 1 says no — this needs no code. Question 2 says yes — it should hold for every task in this repository. Context file.
<!-- AGENTS.md in the repo root -->
## Commands
- Unit tests: `pnpm test:unit`
- Type check: `pnpm typecheck`
- Do not run `npm test`; this repo's default script targets the wrong runner.
You did not need a template, because the trigger is “always”, not “when I ask for a review”.
Worked case 2: “review the diff”
You keep typing the same review prompt.
Decision. Question 1 no, question 2 no — a review is not every task. Question 3: is it more than a screen? Not yet. So: prompt template.
---
description: Review uncommitted git changes
argument-hint: "[focus]"
---
Review the uncommitted git changes (`git status --short`, `git diff`).
Focus on ${1:-correctness, security, and error handling}.
Keep findings short, ordered by severity, each with the file path.
Note the argument substitution. That is the thing a context file cannot express and the reason a template is more than a macro.
Worked case 3: “our release checklist”
The checklist is forty items across three files, it applies only when releasing, and half the items require running a script that validates the tarball.
Decision. Question 1 no — nothing needs to intercept. Question 2 no — it applies only sometimes. Question 3 yes — it exceeds a screen and needs supporting files. Skill.
release-check/
├── SKILL.md
├── references/
│ ├── checklist.md
│ └── versioning-rules.md
└── scripts/
└── validate-tarball.sh
The description is what routes it, so it has to say both what the skill does and when it applies. Chapter 13 covers the routing rules; chapter 14 covers the bundled files.
Worked case 4: “no force pushes, and confirm before anything in migrations/”
Two requirements. The first is text. The second needs to intercept a tool call before it happens.
Decision. Question 1 says yes for the second half — blocking a bash call is runtime behaviour with no text-only equivalent. Extension. And once you are writing one, put both halves in it.
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.on("tool_call", async (event, ctx) => {
if (event.toolName !== "bash" && event.toolName !== "edit") return;
const raw = JSON.stringify(event.input);
if (!/(^|\s)git\s+push\b/.test(raw) && !/migrations\//.test(raw)) return;
if (await ctx.ui.confirm("Allow this call?", raw.slice(0, 120))) return;
return { block: true, reason: "Blocked by guard extension" };
});
}
Chapter 16 covers the event lifecycle; chapter 17 covers the tool contracts this relies on.
Three distinctions that survive all four
Explicit versus routed. A prompt template is invoked: you type /review. A skill is chosen by the model because its description matched. skills.md is candid that this can fail — “A model might fail to load a relevant skill” — and gives /skill:name as the manual override.
Registration versus activation. Registering a tool makes it exist. Declaring it to the model makes it usable. Chapter 6 introduced this; chapter 17 makes it concrete.
Capability versus authority. A tool being registered does not make the model allowed to use it in a given situation, and security.md is blunt that enabled tools run with the operating-system permissions of the Pi process. Deciding whether something is permitted is what an extension’s tool_call handler is for.
The running example
From here to the end of this part we build one thing: a review guard for a service repository. Worked case 2 above is its first version, and each later chapter is that same guard moved up one rung for a stated reason. It starts as text and ends as an extension.
| Chapter | Mechanism | What the running example becomes |
|---|---|---|
| 12 | Prompt template | /review with a focus argument |
| 13 | Skill | A routing description and a short checklist, so the model reaches for it unprompted |
| 14 | Skill with files | The checklist moves to references/, plus a script that runs the repo’s own checks |
| 15 | Extension | A first extension that registers the reviewer’s commands tool |
| 16 | Events | A tool_call handler that blocks force pushes |
| 17 | Tools in depth | The guard gains exposures, nested calls, and annotations |
| 18 | Context | A prompt section that tells the model the guard exists |
Each step is justified by the procedure above, not by preference. If at any step you cannot say which question moved you forward, that step was decoration.
Where the procedure fails
The procedure assumes you can tell whether a need is conditional. Two honest cases where you cannot:
- You do not yet know whether the workflow needs to run a script. Write the skill; if the model cannot run the script without help, that is the signal to move to an extension.
- The need is genuinely two things. Write both. A context file and a prompt template are not rivals, and a project with an
AGENTS.mdstating the repo’s conventions plus a/reviewtemplate is the normal case, not a compromise.
The cheapest rung is the one people skip, because saving a prompt’s wording feels too small to count as a mechanism. It is one: a file, an optional argument, and a / command. Knowing precisely what it can and cannot do is also what tells you when you have outgrown it — which is why the lowest rung comes before the heavier ones.