Course · Pi · Skills & packages
Write the content-source-onboarding procedure as a SKILL.md
Every time a teammate wants stash to save from a source it doesn’t handle yet, you’ve been doing the same five things by hand: look at a sample page from the new source, work out what its markup does to the readability extractor, write a small adapter, register it with the fetch worker, run it against a real URL and check the article text came out clean instead of full of nav cruft. You’ve re-explained that procedure to Pi twice now, typing out the same steps in slightly different words each time. That’s the tell. When you catch yourself re-narrating a multi-step routine instead of just naming it, it’s time to write it down once - and this time the right format for “write it down” isn’t TypeScript.
Skill and extension are different layers
Section titled “Skill and extension are different layers”An extension - what you built in the last two modules - is a TypeScript module that registers a tool, a command, or a hook through the ExtensionAPI. It’s code: it can enforce, block, compute, reach out to the fetch worker directly. A Skill is the layer below that. It’s markdown with a bit of YAML on top, and it can’t do anything an extension couldn’t already do more directly - a Skill has no execution semantics of its own beyond “here are the instructions, go follow them” (though its body can absolutely tell the agent to run a script that ships alongside it). What a Skill buys you instead is that you can write one without touching TypeScript, and that it stays nearly free in context until the moment it’s actually relevant.
An extension enforces; a Skill only informs. Reach for a Skill when the job is a procedure worth naming, not a rule that has to hold on every tool call.
That second part is worth planting a number against before you see the file. Once you do, count two things in it: the words between the two --- frontmatter markers, and the words below the second one. Here they are: 62 words of frontmatter, 186 words of body - exactly three times as long. At startup, Pi only ever pays for the first number. Whether it pays for the second depends entirely on whether your request matches the description. That’s the mechanism worth understanding before you write one: progressive disclosure. At startup, Pi scans every Skill it can find and pulls only two fields - name and description - into the system prompt. The rest of the file, and anything else in the Skill’s directory, stays on disk. Only when your request matches a Skill’s description does Pi reach for its read tool and pull in the full body.
The file structure
Section titled “The file structure”A Skill is a directory with one required file inside it:
onboard-content-source/├── SKILL.md (required)├── scripts/├── references/└── assets/scripts/, references/, and assets/ are optional siblings - a helper script the body tells Pi to run, a longer reference doc, a template file to copy. None of it loads until the body of SKILL.md actually points at it. For the onboarding procedure, a single SKILL.md covers it; you reach for the subdirectories only once a Skill’s instructions genuinely outgrow one file.
Pi looks for Skill directories in several places, checked in order:
~/.pi/agent/skills/and~/.agents/skills/- global, yours across every project.agents/skills/- discovered by walking up from your current directory to the repo root.pi/skills/and.agents/skills/in the project itself- a
skills/directory (orpi.skillskey) inside an installed package - the subject of the packages lesson - a path passed with
--skill <path> - a
skillsarray insettings.json
Since this procedure is stash-specific - it references the fetch worker and the readability extractor by name - it belongs in the repo, in .pi/skills/ or .agents/skills/, committed so anyone who clones stash gets it too. A dozen Skills sitting in that directory cost you the price of one - whichever one actually fires.
Frontmatter: the label Pi reads before it opens the box
Section titled “Frontmatter: the label Pi reads before it opens the box”Here’s the whole Skill:
---name: onboard-content-sourcedescription: >- Onboard a new content source into stash's fetch worker when a URL from an unfamiliar site or platform isn't extracting cleanly - nav/ads leaking into the article text, or the extractor returning empty content. Use when the user wants to add support for a new site, wire up a new source, or fix a source that's extracting badly.---
Follow these steps when a source needs onboarding or repair. Ask for areal sample URL from the source if one isn't already in the request -do not guess at a site's markup.
1. Fetch the sample URL and inspect the raw HTML structure around the article body: what wraps the real content, what wraps nav/ads/related links.2. Check whether the current extractor already gets close. If it's leaking specific boilerplate (a cookie banner, a related-posts block), that's usually a targeted selector fix, not a new adapter.3. If the site's shape is different enough to need its own path, write a small source adapter under the extractors directory, following the structure of the existing ones.4. Run the adapter against the sample URL. Confirm the stored article text has no leftover nav/ads and no missing paragraphs.5. Confirm the new entry is searchable: check it shows up in a full-text search for a distinctive phrase from the article body.6. Report back a short diff of raw HTML vs. extracted text so the user can eyeball the extraction before it goes live for everyone.Count it yourself if you want to check the number planted above: 62 words above the second ---, 186 in the six numbered steps below it, 248 total. Three-quarters of this file’s words never reach the system prompt unless a real request matches the description first.
Only two fields are required, and they’re the only two doing any work at Skill-selection time:
name- 1 to 64 characters, lowercase letters, numbers, and hyphens only. Unlike the plain Agent Skills standard, Pi doesn’t require this to match the directory name - a deliberate deviation, so a shared skills repo can be reused across several tools without renaming folders to suit each one.description- up to 1024 characters, and it’s the entire trigger surface. Pi’s system prompt carries this string for every Skill it knows about; when your request semantically matches it, the Skill fires. Notice the description above names the situation (“a URL from an unfamiliar site isn’t extracting cleanly”) rather than just an action - that’s what lets it match “hey can we add support for this newsletter site” without you ever typing the Skill’s name.
A handful of optional fields round out the spec: license, compatibility (environment requirements), metadata (arbitrary key/value), allowed-tools (a pre-approved tool list for this Skill), and disable-model-invocation - set that last one and the Skill drops out of the system prompt entirely, invocable only by hand. None of them change whether the Skill can fire; they change how much you trust it to fire on its own.
Two fields decide whether the Skill fires at all; everything else just narrows how much you trust that decision.
Invoking it
Section titled “Invoking it”Once it’s saved, you can call it directly:
> /skill:onboard-content-source> /skill:onboard-content-source https://example-newsletter.com/p/some-postor just describe the problem and let the description do the matching:
> stash is choking on this Substack link, the article text has the subscribe-box and comments mixed into itEither way Pi’s read tool is what actually loads the body - you can watch that happen in whatever context view you built two modules back. The /skill: slash-command surface itself is a toggle: enableSkillCommands in settings.json controls whether the explicit /skill:name form exists at all, separate from whether the model can invoke a Skill on its own recognition.
Here’s the shape of a SKILL.md, and where it sits relative to how other agents handle the same idea. name and description are required everywhere - no surprise there, that’s the trigger. Before you click through: guess which field only three of the five tools support - the one that lets you say “never fire this on your own,” full stop.
Three of five: Claude Code, Cursor, and Copilot all read disable-model-invocation. Codex reaches the same outcome through a sidecar file instead (policy.allow_implicit_invocation: false), and OpenCode has no documented equivalent at all - a Skill you write for OpenCode can’t be told to sit fully manual.
What’s actually out there
Section titled “What’s actually out there”Pi didn’t invent this format. SKILL.md is the same Agent Skills standard Claude Code and Codex CLI read, and Pi’s own maintainers publish a landing point for it: badlogic/pi-skills on GitHub, billed explicitly as compatible with Claude Code and Codex too. That’s the same portability the name-doesn’t-have-to-match-folder deviation exists for - one skills repo, reused across tools without renaming a single folder. Worth a look before you write one from scratch: someone may already have onboarded the exact content-source shape you’re staring at.
The security line, same as any code you didn’t write
Section titled “The security line, same as any code you didn’t write”The docs are blunt about this, and it’s worth taking at face value: a Skill’s body is instructions the model will follow, and it can tell Pi to run arbitrary scripts sitting right next to it in scripts/. Installing someone else’s Skill - which the next-but-one lesson is entirely about - deserves the same read-before-you-trust caution you’d give an extension. A Skill being “just markdown” doesn’t mean it’s inert.
What a Skill can’t do
Section titled “What a Skill can’t do”Name the cost this format creates before you move past it: a Skill can’t force anything. It fires when the model’s read of your request matches the description, and that’s a judgment call, not a guarantee - too broad a description and it fires on something adjacent but wrong; too narrow and it silently doesn’t fire on the exact case you wrote it for. There’s no way to tell a Skill “always run this check before touching the database,” the way the damage-control gate two modules back holds on every single tool_call whether the model likes it or not. If a stash chore needs that kind of unconditional enforcement, a Skill is the wrong layer - you’re back to a hook. What a Skill buys you is cheap, on-demand instructions for a procedure. What it can’t buy you is certainty that the procedure runs at all.
| Layer | What it costs | What it buys | Who decided |
|---|---|---|---|
| Skill | Frontmatter only (62 words here) at startup; the body (186 words) loads only if a request matches | Removes re-typing a five-step procedure by hand every time a new source shows up | You wrote the description; the model decides when it matches |
Two lighter no-code layers are worth the same five minutes each - a shortcut for the prompt you type often, and a theme for the terminal you’re staring at all week. Neither one tries to fire on its own the way this Skill does. Next: templates and themes.