Course · Pi · Skills & packages
Prompt templates and themes: the lighter no-code layer
The Skill you just wrote fires on its own when a request matches its description - that’s the point of it, the whole reason it’s worth the frontmatter. Not everything you’d like to save typing on deserves that. Some things are just a prompt you find yourself retyping verbatim, with one or two words swapped each time, and for those Pi has something simpler than a Skill: a prompt template, invoked by hand, no auto-detection, no description field doing pattern-matching.
Prompt templates: a markdown snippet with blanks
Section titled “Prompt templates: a markdown snippet with blanks”A prompt template is a plain markdown file. No auto-invocation - you type the slash command yourself, every time. Say you keep asking Pi to triage a batch of failed stash fetches the same way: show the URL, the error, and a guess at whether it’s a rate-limit problem or a genuinely broken extractor. Instead of typing that framing out again:
---description: Triage a batch of failed stash fetch-worker jobsargument-hint: "[since]"---
Look at stash's fetch-worker failures ${1:-in the last 24 hours}. Foreach failed URL, report: the error message, whether it looks like arate limit or a real extraction failure, and one recommended next step(retry, skip, or needs a new source adapter).Frontmatter here is optional and thinner than a Skill’s: description (if you skip it, Pi just uses the file’s first non-empty line) and argument-hint, which is what shows up in autocomplete - angle brackets for a required argument, square brackets for optional ones. The body is where the argument syntax does its work:
| Syntax | Meaning |
|---|---|
$1, $2, … | positional arguments, in the order you typed them |
$@ or $ARGUMENTS | every argument you passed, joined into one string |
${1:-default} | falls back to default if that argument wasn’t supplied |
${@:N} / ${@:N:L} | a slice of the arguments starting at position N, optionally limited to length L |
Type /triage-fetch and ${1:-in the last 24 hours} falls back to its default; type /triage-fetch "since last Tuesday" and $1 becomes that string instead. Invocation is just the filename without its extension - /triage-fetch, same pattern as any other slash command you’ve already used.
Templates live in the same kind of tiered locations as everything else in Pi: ~/.pi/agent/prompts/*.md globally, .pi/prompts/*.md per project (gated behind project trust, same as extensions), inside an installed package’s prompts/ directory, listed in a settings.json prompts array, or handed in directly with a repeatable --prompt-template <path> flag.
The distinction that matters: a Skill’s description is read by the model, deciding on your behalf whether to pull the whole thing in. A prompt template’s description is read by you, in autocomplete, deciding whether to type the slash command. If you find yourself wanting a template to fire automatically when a situation arises rather than when you type it - that’s your sign it should have been a Skill all along.
A template costs nothing until you type it, and buys you nothing unless you remember to. That trade cuts both ways, which is the honesty worth naming here: triage-fetch.md doesn’t know when it’s stale. Its ${1:-in the last 24 hours} blank assumes the fetch worker’s failure log takes a plain time-window string. If that log’s shape ever changes - say the worker moves to a structured --source X --since Y filter instead of a freeform string - nothing tells the template it’s wrong. Pi expands ${1:-in the last 24 hours} exactly as written and hands the model a framing that no longer matches the tool underneath it. A Skill’s body at least gets re-read fresh by an agent that might notice something’s off along the way; a template’s expansion drops straight into the prompt, unchecked against whatever it’s actually driving. Update the template the same day you change the command it wraps, or it quietly starts lying to you.
Themes: the terminal you’re staring at for eight hours
Section titled “Themes: the terminal you’re staring at for eight hours”Unrelated to either of the above, but worth the five minutes here because it’s the same “just a file” shape: a Pi theme is a JSON file describing the TUI’s color palette - not just an accent color, a full token set covering syntax highlighting, message backgrounds, tool-output states, even a distinct border color per thinking-effort level. The real schema runs 51 tokens (count them yourself against Pi’s own dark.json if you want to check it) - too many to walk one at a time, so trace the handful you’d actually watch scroll by during the onboarding session from the last lesson instead.
Here’s the slice that matters for that trace, from a stash-dark theme:
{ "name": "stash-dark", "vars": { "primary": "#e0a458", "secondary": 242 }, "colors": { "text": "", "toolSuccessBg": "#1e2e1e", "toolErrorBg": "#2e1e1e", "toolDiffAdded": "#00ff00", "toolDiffRemoved": "#ff0000" }}Two of those choices are reasoned from what stash actually does, not picked blind. primary is a warm amber (#e0a458), not the stock blue-white most themes default to - stash is a read-it-later tool, and the screen you’re staring at for the longest stretch is extracted article text, not code. Amber-on-dark is the same low-blue-light choice e-readers make for exactly that reason. And toolDiffAdded / toolDiffRemoved stay a loud, saturated green and red rather than something softer - because step 3 and step 6 of the onboarding procedure are both diffs you’re supposed to actually read, confirming the extractor didn’t mangle anything. A muted diff color is exactly how a bad merge slips past a tired reviewer at hour six.
text is left as an empty string on purpose - it falls back to whatever your terminal already uses, since body text is the one thing you don’t want a theme second-guessing. toolSuccessBg and toolErrorBg are the pending-call backgrounds you watch on every one of the six onboarding steps, since every one of them is a tool call.
Now predict before you look: swap primary from amber to something else entirely. Of the five tokens above, how many change?
None of them. Not one of text, toolSuccessBg, toolErrorBg, toolDiffAdded, or toolDiffRemoved references "primary" - they’re all independent literal values. Swapping primary only moves the tokens that actually point at it, and in the full 51-token file, that’s 8: accent, border, customMessageLabel, toolTitle, mdLink, syntaxKeyword, syntaxOperator, thinkingMinimal. “Primary” describes what the color is for, not how much of the theme depends on it - 43 of the 51 tokens don’t care what you set it to.
A vars entry is a shortcut for typing one hex string in eight places, not a dial that controls the theme.
The same walkthrough answers a bigger question: of the theme’s 51 tokens, how many actually render during one real run of the onboard-content-source procedure, and how many just sit there? Walk the six steps. Every one is a tool call, so the five tool-status tokens (toolPendingBg, toolSuccessBg, toolErrorBg, toolTitle, toolOutput) fire from step one. Step 3 writes a new adapter and step 6 reports a diff, so the three diff tokens fire too. The adapter is TypeScript, so the nine syntax* tokens light up once it’s shown on screen. Add the always-on chrome around all of that - accent, border, borderAccent, borderMuted, text, dim, muted, userMessageBg, userMessageText, thinkingText (10 tokens) - and exactly one of the six thinking* levels, whichever defaultThinkingLevel you’re running. That’s 28 tokens guaranteed to render before the procedure is even half done.
13 more never render in this procedure, full stop: the three customMessage* tokens exist for extensions that push their own message type, and nothing in a plain Skill does that; selectedBg needs a picker menu the procedure never opens; bashMode needs you typing straight into a shell prompt instead of describing the task in English; the standalone success, error, and warning tokens (distinct from the tool*Bg ones already counted) need a banner nothing in this procedure raises; and the other five thinking* levels sit dark because only one is ever active at a time.
That leaves 10 - the md* tokens for headings, links, quotes, and bullet lists - genuinely conditional on whether the model’s own final report happens to use that formatting. Total it: 28 plus 13 plus 10 is 51. Most of a theme file isn’t “sometimes” at all; it’s decided the moment the session starts, one way or the other, by what kind of turn is actually running - only about a fifth of the tokens are up to the model’s prose on the day.
None of this is measured from Pi’s own telemetry - Pi doesn’t publish per-session paint counts. It’s a walk through the same six steps from the last lesson against the same 51 names, and you can redo it yourself against a different procedure and get a different split.
name can’t contain a forward slash. Each color value can be a hex string, a 256-color palette index, a vars name, or an empty string to fall back to whatever your terminal already uses.
Pi ships two built-in themes, dark and light, and auto-picks one at first launch by detecting your terminal’s background. Beyond those, discovery follows the same tiered pattern: ~/.pi/agent/themes/*.json globally, .pi/themes/*.json per project, a package’s themes/ directory, a settings.json theme array, or --theme <path> on the command line - and --no-themes turns the whole discovery pass off if you just want the two built-ins. Switch with /settings or by setting "theme": "stash-dark" directly in settings.json. One nice touch worth knowing about if you end up tweaking one: editing the file behind your currently active custom theme hot-reloads it immediately - no restart, so you can nudge a color and watch it land in the same terminal.
Zero of a theme’s 51 tokens ever reach the model. They cost nothing in the window, which also means they buy nothing the model needs - a theme is a human-comfort layer, full stop, and that’s fine.
The real leverage in this module is what you built in the last lesson and what you’re about to do with it: hand it to someone else.
| Layer | What it costs | What it buys | Who decided |
|---|---|---|---|
| Template | Nothing until typed - no startup listing at all | Removes retyping a recurring one-off prompt verbatim | You, always - no model discretion |
| Theme | Nothing in the window, ever - a rendering cost, not a token cost | Nothing the model needs; comfort for the person staring at the screen | You, entirely - the loop never touches it |
Next: package the skill, the extensions, and the templates, and share the whole thing.