← Blog

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:

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:

FieldWhat it's for
licenseLicense name or a path to a bundled license file
compatibilityUp to 500 chars on environment needs (packages, network, intended product)
metadataString-to-string map. Put author and version here
allowed-toolsPre-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:

  1. What it does, in verbs. "Extracts, fills, merges."
  2. When to use it, starting with "Use when..."
  3. Trigger words users say. Include synonyms. "invoice" and "receipt" if both apply.
  4. 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:

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:

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:

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.

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

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.