There’s a quiet cost to giving an agent lots of tools or workflows: each one consumes context. With ten skills at ~400 tokens each, you’ve burned 4 K tokens before the model has done anything. With thirty, you’re at 12 K. On a phone, where you’re running a 1.7B–8B on-device model with a 4 K–8 K context, this is a problem.
Skills solve it with progressive disclosure.
What the model sees
At the start of a run, the orchestrator builds a system prompt that includes a skills index — just the name
and description for each skill. That’s typically 20–30 tokens per skill. With 30 skills, that’s ~600 tokens.
# Available skills
- `summarise-email` — Summarise an email into 3 bullet points.
- `triage-notifications` — Score notifications by urgency.
- `compose-reply` — Draft a reply in the user's voice.
- `extract-receipt` — Parse a photo of a receipt into structured fields.
- ... (26 more)
That’s it. The bodies — the actual step-by-step instructions, the tool usage hints, the example outputs — are nowhere
in context. They sit on disk at /workspace/.skills/<name>/SKILL.md.
What happens on invocation
When the model decides it wants a skill, it emits a tool call:
Sh("skill invoke summarise-email")
// or directly
{ "tool": "skill.invoke", "args": { "name": "summarise-email" } }
The skill.invoke tool reads the bundle, injects its body as a new system-message turn, and the model continues with
the skill’s instructions front-and-center. After the skill finishes (often a single turn or a few), the body is
evicted from context — only the skill’s output is retained.
So a 30-skill catalog uses 600 tokens at the index level, plus 400 tokens for whichever single skill is active. 1 K tokens total instead of 12 K.
Writing a skill
A skill is a directory with at least a SKILL.md:
---
name: summarise-email
description: Summarise an email into 3 bullet points.
---
# Steps
1. Read the email body via `/bin/gmail.show --id $1`.
2. Extract the subject, sender, and the 3 most important sentences.
3. Emit JSON: `{ subject, from, bullets: [...] }`.
# Tools
- `/bin/gmail.show`
- `/bin/jq`
# Example output
\`\`\`json
{
"subject": "Q3 board prep",
"from": "ceo@example.com",
"bullets": [
"Q3 numbers come in Tuesday — board deck due Wednesday morning.",
"Marketing's launch slipped — replace section 4 with a roadmap slide.",
"Investor list needs updating — add the two new participants from June."
]
}
\`\`\`
That’s all that’s needed. The orchestrator picks it up next time it scans /workspace/.skills/.
Versioning and discovery
Skills are just files. Ship them in the app bundle, sync them from a server, let the user write their own. agentlib doesn’t care; it just scans the directory.
For app-shipped skills, copy them into the workspace on first launch:
final dir = Directory('${docsDir.path}/workspace/.skills/summarise-email');
await dir.create(recursive: true);
final asset = await rootBundle.loadString('assets/skills/summarise-email.md');
await File('${dir.path}/SKILL.md').writeAsString(asset);
For user-written or remotely-synced skills, just write the files; the next agent run sees them.
Skill composition
A skill’s body can call out to other skills:
# Steps
1. If the email is from the user's manager, invoke `triage-urgent` first.
2. Otherwise summarise per the standard pattern.
3. If the summary is longer than 3 bullets, invoke `compress-bullets`.
The orchestrator handles the dispatch. This lets you build a small tree — one “front-door” skill that knows when to delegate.
When skills are wrong
Skills are great for procedures the agent does often. They’re not the right shape for:
- Data (use the VFS —
/workspace/<file>). - Tools (register a CLI in
/bin/). - System prompt material that should always be in context (just put it in the orchestrator’s
instructions). - Per-user memory (use
/memory— the SQLite-backed mount).
If you find yourself loading the same skill on every turn, it’s probably not a skill — it’s an instruction.
Reference
- The default skills directory is configured on
RunConfig. Override withRunConfig(skillsDir: ...). SkillRegistryis the in-memory cache. Callregistry.reload()if you wrote a new skill mid-run.- Skill invocation goes through the same hook pipeline as any tool call —
PreToolUseandPostToolUsefire onskill.invoke.
Read next
- Subagents at scale — when you need full agents, not just procedures.
- CLI-first agent design — the Sh idiom that pairs with skills.