How to Write a SKILL.md That Agents Actually Pick Up
October 31, 2026 · backskills · 9 min read
A SKILL.md is the one required file in an agent skill: YAML frontmatter with a name and a description, followed by Markdown instructions. To write one that agents actually use, make name match the folder, write a description that says what the skill does and when to use it, and keep the body short enough to load cheaply.
The details below. A skill is a folder. Inside it sits one required file, SKILL.md, and maybe some scripts, references, and templates. That's the whole format.
Simple to write. Easy to write badly.
Most skills that fail don't fail because the instructions are wrong. They fail because the agent never loads them, or loads them at the wrong moment, or loads them and drowns in 4,000 lines of context it didn't need. This guide walks through the format as defined in the open Agent Skills specification, then gets into the parts the spec leaves to you.
The minimum viable skill
pdf-processing/
├── SKILL.md
├── scripts/
│ └── extract_tables.py
└── references/
└── form-fields.md
And the file itself:
---
name: pdf-processing
description: Extracts text and tables from PDF files, fills PDF forms, and merges multiple PDFs. Use when working with PDF documents or when the user mentions PDFs, forms, or document extraction.
---
# PDF processing
## Extract text
Run `python scripts/extract_tables.py <file>` and read the JSON it prints.
## Fill a form
Read `references/form-fields.md` first. Field names are case-sensitive.
Two fields are required. Everything after the second --- is Markdown instructions, and the spec puts no format restrictions on it.
Frontmatter rules you will trip on
The spec is strict about name:
- 1 to 64 characters
- lowercase letters, digits, hyphens only
- no leading or trailing hyphen, no
-- - must match the parent directory name
That last one bites people. Rename the folder, forget the frontmatter, and some loaders will reject the skill outright. Microsoft's Agent Framework docs are explicit that invalid YAML, duplicate fields, or wrong casing "prevent the skill from loading." Silent failure. No error in your face, just an agent that never uses your work.
Optional fields:
| Field | What it's for |
|---|---|
license | License name or a path to a bundled license file |
compatibility | Up to 500 chars on environment needs (packages, network, intended product) |
metadata | String-to-string map. Put author and version here |
allowed-tools | Pre-approved tools. Experimental, support varies |
Put a version in metadata. The spec doesn't force it. Do it anyway:
metadata:
author: your-org
version: "1.3.0"
Quote the version. YAML will happily turn 1.10 into the float 1.1, and the spec says metadata values are strings.
The description does all the triggering
Agents load skills progressively. Per the spec, only name and description (roughly 100 tokens) are loaded at startup for every installed skill. The body loads when the agent decides the skill is relevant. Scripts and references load only when the body points at them.
So the description is the only thing the agent sees when it decides whether to open your skill. The agentskills.io guide to optimizing descriptions puts it bluntly: "the description carries the entire burden of triggering."
A bad one:
description: Helps with PDFs.
A good one names the actions, the objects, and the words a user might actually type:
description: Extracts text and tables from PDF files, fills PDF forms, and merges multiple PDFs. Use when working with PDF documents or when the user mentions PDFs, forms, or document extraction.
A pattern that works:
- What it does, in verbs. "Extracts, fills, merges."
- When to use it, starting with "Use when..."
- Trigger words users say. Include synonyms. "invoice" and "receipt" if both apply.
- When not to use it, if there's a nearby skill it gets confused with. "Not for scanned images; use ocr-images."
You have 1,024 characters. Most good descriptions use 200 to 400.
Test the trigger, not just the instructions
Write down ten requests that should load the skill and five that shouldn't. Run them. If "merge these two PDFs" doesn't open the skill, your description is missing "merge." This is boring and it catches more bugs than anything else in this post.
# should-trigger.txt
merge these two pdfs into one
pull the table out of page 3 of this report
fill in this W-9 for me
...
# should-not-trigger.txt
convert this docx to markdown
summarize this web page
Keep these files in the repo next to the skill. They become your regression suite later.
Keep the body short. Push detail down.
The spec recommends keeping the body under 5,000 tokens, and Microsoft's guidance says to keep SKILL.md under 500 lines. Once activated, the whole body goes into context. Every paragraph you add costs tokens on every single activation.
Use the folder:
scripts/for anything deterministic. Parsing, validation, API calls. Code is cheaper and more reliable than prose describing code.references/for long docs the agent only sometimes needs. Field lists, API references, style guides.assets/for templates and static files.
Then reference them by relative path from the body, with a sentence on when to read them:
## Filling forms
Before filling any form, read `references/form-fields.md`.
It lists every field name and its expected format.
"Read X before Y" beats "see X for more info." The agent needs a reason to spend the tokens.
Write instructions like a runbook
The body has no required structure. What works in practice looks like an on-call runbook, not an essay.
## Merge PDFs
1. Confirm the input order with the user if more than 2 files.
2. Run: `python scripts/merge.py out.pdf in1.pdf in2.pdf`
3. If it exits with code 2, a file is encrypted. Ask for the password. Don't guess.
4. Report the output page count.
### Edge cases
- Mixed page sizes: keep originals, don't rescale.
- Over 500 pages: warn the user before running.
Numbered steps. Exact commands. Explicit failure handling. An example input and output if the format is fussy.
Things to cut:
- Background on what a PDF is. The model knows.
- Motivational framing. "You are an expert..." rarely earns its tokens.
- Instructions that only matter for one model. If you need them, see the note on models below.
Scripts: make them loud
When a script fails, the agent only knows what you print. So print useful errors and use exit codes.
#!/usr/bin/env python3
"""Merge PDFs. Usage: merge.py OUT IN [IN ...]"""
import sys
from pypdf import PdfReader, PdfWriter
def main(argv: list[str]) -> int:
if len(argv) < 3:
print("usage: merge.py OUT IN [IN ...]", file=sys.stderr)
return 1
out, *inputs = argv[1:]
writer = PdfWriter()
for path in inputs:
reader = PdfReader(path)
if reader.is_encrypted:
print(f"ENCRYPTED: {path}", file=sys.stderr)
return 2
for page in reader.pages:
writer.add_page(page)
with open(out, "wb") as f:
writer.write(f)
print(f"OK pages={len(writer.pages)} out={out}")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
Declare dependencies in compatibility (Requires python3 and pypdf) so a user installing the skill knows before it breaks.
Same skill, different models
A skill that works well on one model can behave differently on another. That isn't a hunch. Research on prompt sensitivity, like Sclar et al., 2023, found that small formatting changes in a prompt can swing accuracy by up to 76 points (LLaMA-2-13B, few-shot), and that format performance only weakly correlates between models. Your SKILL.md body is a prompt. It inherits that.
Practical moves:
- Prefer scripts over prose for anything that must be exact.
- Run your should-trigger list on every model you claim to support.
- If you do need model-specific guidance, say so in
compatibilityrather than hiding it.
A full post on model variance is coming next in this series.
Versioning: future you will thank you
Skills change. You'll fix a bug, tighten the description, add a script. Every one of those changes what agents do, for everyone using the skill.
- Bump
metadata.versionon every change that alters behavior. - Use semver loosely: patch for wording fixes, minor for new capability, major when old triggers stop working or outputs change shape.
- Keep a
CHANGELOG.mdin the skill folder. Short is fine. - Tag releases in git so people can pin to a commit.
People who depend on your skill need to know when it changed. That's the problem backskills tracks: it reads public SKILL.md files on GitHub and servers in the MCP Registry, checks them daily, keeps version history, and sends signed webhook alerts within 24 hours when a watched skill changes. More in How to track skill changes and pin versions.
Pre-publish checklist
- [ ]
namematches the folder, lowercase, no double hyphens - [ ] YAML parses (
python -c "import yaml,sys; yaml.safe_load(open('SKILL.md').read().split('---')[1])") - [ ] Description says what, when, trigger words, and when not
- [ ] Body under ~500 lines; long material moved to
references/ - [ ] Every script prints clear errors and uses exit codes
- [ ]
compatibilitylists real dependencies - [ ]
metadata.versionset and quoted - [ ] should-trigger / should-not-trigger lists checked on each target model
- [ ] CHANGELOG entry written
A quick validator you can drop into CI:
import re, sys, pathlib, yaml
NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
def check(skill_dir: pathlib.Path) -> list[str]:
errs = []
text = (skill_dir / "SKILL.md").read_text()
parts = text.split("---", 2)
if len(parts) < 3:
return ["missing frontmatter"]
fm = yaml.safe_load(parts[1]) or {}
name = fm.get("name", "")
if not NAME_RE.match(name) or len(name) > 64:
errs.append(f"bad name: {name!r}")
if name != skill_dir.name:
errs.append(f"name {name!r} != folder {skill_dir.name!r}")
desc = fm.get("description", "")
if not (1 <= len(desc) <= 1024):
errs.append("description must be 1-1024 chars")
if len(parts[2].splitlines()) > 500:
errs.append("body over 500 lines")
return errs
if __name__ == "__main__":
bad = 0
for d in map(pathlib.Path, sys.argv[1:]):
for e in check(d):
print(f"{d}: {e}"); bad = 1
sys.exit(bad)
Once it's out
Publish it somewhere public, like a GitHub repo with the skill folder at a stable path. Add a badge to your README so users can see version and status at a glance (backskills offers author badges for this). And if you also ship an MCP server, list it in the official MCP Registry.
The format is small on purpose. Most of the craft is in the description, the tests, and the discipline to version every change.
FAQ
What fields are required in SKILL.md frontmatter? Only name and description. license, compatibility, metadata, and allowed-tools are optional.
Why isn't my skill triggering? Usually the description. The agent decides from name and description alone, so add the verbs and words users actually type, and test with a should-trigger list.
How long can the description be? 1 to 1,024 characters per the spec.
Can I use the same SKILL.md across agents? The format is an open spec, so the file is portable. Behavior still varies by model and client, so test on each one you support.
What's the difference between SKILL.md and an MCP server? See SKILL.md vs AGENTS.md vs CLAUDE.md vs MCP.
Track any public skill for changes, free: backskills.ai. Agents can use the MCP endpoint at https://backskills.ai/mcp or read llms.txt.