> This page is for Agent Handler.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.merge.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.merge.dev/_mcp/server.

# Writing a skill

> How to author a skill for Agent Handler: SKILL.md frontmatter, bundled reference files, size limits, validation rules, and authoring patterns that work.

A skill is a `SKILL.md` file. Everything else is optional. Write the instructions you would give a capable new hire who has your tools but not your context, then upload the file.

This page covers the format Agent Handler validates on upload and the authoring choices that decide whether an agent finds your skill and follows it.

## The smallest valid skill

**`SKILL.md`**

```markdown SKILL.md
---
name: Expense report review
description: Review a submitted expense report against the T&E policy and either approve it or send it back with the specific line items that need receipts. Use when someone asks to review, approve, or check an expense report.
---

# Expense report review

## Steps

1. Pull the report with `expensify__get_report`. Note the submitter and total.
2. Check every line item over $75 for an attached receipt.
3. Check the report against the T&E limits in `references/te-policy.md`.
4. Approve if every line passes. Otherwise reject with the failing line items named individually.

## Rules

- Never approve a report you submitted yourself. Hand those to the submitter's manager.
- A missing receipt is a send-back, not a judgment call.
- Quote the exact policy line when you reject something, so the submitter knows what to fix.
```

Upload that file on its own. You do not need to zip a single-file skill.

## Frontmatter

`SKILL.md` starts with YAML frontmatter between `---` lines, then the Markdown body.

| Key                     | Required | What it does                                                                                                                                  |
| ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                  | Yes      | Display name. Must be a non-empty string. Also the default source of the skill's slug                                                         |
| `description`           | Yes      | Must be a non-empty string. Shown on the card and returned by `list_skills`, and the main thing that decides whether an agent picks the skill |
| `referenced_connectors` | No       | List of connector slugs the skill expects, for example `["expensify", "slack"]`. Surfaces the connector logos on the card                     |

Any other keys you include are preserved on the version record but Agent Handler does not act on them.

`referenced_connectors` is a declaration, not a grant and not a precondition. Slugs are not checked against your connected connectors, so a typo shows up as a missing logo rather than an error, and naming a connector never gives the agent access to it.

## Write the description for the match, not for the reader

`list_skills` returns each skill's name and description, and its keyword filter matches on those two fields only. The body of your skill is never searched. So a description that reads well but omits the words people actually use makes the skill invisible.

Say what the skill does, then say when to use it, in the vocabulary of the person asking.

```yaml
# Weak: accurate, unmatchable
description: Handles the review workflow for submitted reports.

# Strong: names the object, the action, and the trigger phrases
description: Review a submitted expense report against the T&E policy and either approve it or send it back with the line items that need receipts. Use when someone asks to review, approve, or check an expense report, or mentions a reimbursement that is stuck.
```

Two skills whose descriptions overlap heavily will both match, and the agent has to guess. If you find yourself writing near-identical descriptions, that is usually one skill with a branch in the body, not two skills.

## Bundling reference files

Put anything long, rarely needed, or reference-shaped in a separate file and point to it from the body. Agents read `SKILL.md` first and receive the list of bundled files with it; they fetch a bundled file only when they need it.

```
expense-report-review.zip
├── SKILL.md                       # entry point: steps, rules, pointers
├── references/
│   ├── te-policy.md               # the full policy text
│   └── escalation-matrix.md       # who approves what over which amount
└── examples/
    └── rejection-message.md       # the tone to use when sending one back
```

Zip the bundle with `SKILL.md` at the root, not nested inside a folder. Reference bundled files by their relative path in the body (`references/te-policy.md`), because that path is exactly what the agent passes back to fetch the file.

The split to aim for: `SKILL.md` holds the procedure and the decisions, and bundled files hold the material the procedure consults. A `SKILL.md` that inlines a 4,000-word policy costs every agent that context on every retrieval, whether it needs the policy or not.

## Limits

Agent Handler validates the bundle on upload and rejects it whole if anything fails. Nothing in a bundle is ever executed.

| Limit                     | Value             |
| ------------------------- | ----------------- |
| Upload size, compressed   | 10 MB             |
| Bundle size, uncompressed | 10 MB             |
| Any single file           | 2 MB              |
| File count                | 100               |
| `SKILL.md` length         | 20,000 characters |

`SKILL.md` must be UTF-8 and must sit at the root of the bundle. Symlinks are rejected. Paths that try to escape the bundle (absolute paths, `..` segments, Windows drive prefixes) are rejected. An archive whose uncompressed size exceeds its compressed size by more than 100x is rejected.

The 20,000 character cap on `SKILL.md` is a design constraint as much as a safety one. If your entry point is pushing it, the material that pushed it belongs in a bundled file.

## What gets rejected, and what to fix

| Error                                                          | Cause                                                                                                              |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `Bundle must contain a SKILL.md at its root`                   | `SKILL.md` is nested in a folder, or named differently. Re-zip from inside the skill directory                     |
| `SKILL.md must start with YAML frontmatter delimited by '---'` | Missing or malformed `---` fences. The opening fence must be the first line                                        |
| `SKILL.md frontmatter is not valid YAML`                       | Usually an unquoted colon inside a description. Quote the value                                                    |
| `SKILL.md frontmatter is missing a non-empty 'name'`           | `name` or `description` is absent, blank, or not a string                                                          |
| `frontmatter 'referenced_connectors' must be a list of slugs`  | Written as a comma-separated string instead of a YAML list                                                         |
| `The 'merge-' slug prefix is reserved`                         | Your skill's name slugifies to something starting with `merge-`. Rename the skill, since the slug follows the name |
| `Symlinks are not allowed in a skill bundle`                   | The zip preserved a symlink. Copy the real file in instead                                                         |

You can check a bundle before committing to it: the upload flow validates and previews the parsed name, slug, description, and file list before anything is saved. See [Publishing skills](/merge-agent-handler/build/skills/publishing-skills).

## Patterns worth copying

* **Lead with when to use it.** The first thing in the body should tell an agent whether it is in the right place. An agent that retrieved the wrong skill should be able to tell immediately.
* **Number the steps.** Ordered steps get followed in order. A prose paragraph describing the same sequence gets summarized.
* **Name real tools.** Writing `expensify__get_report` rather than "the Expensify API" removes a guess. If the tool name changes, the skill is wrong in an obvious way rather than a subtle one.
* **Write the rules as rules.** "Never approve your own report" belongs in its own line under a Rules heading, not buried in step 4.
* **State the stop conditions.** Say what the agent should do when the data is missing or ambiguous. Absent that, it will pick something.

## What to avoid

* **Do not put credentials, tokens, or keys in a skill.** A skill is readable by every member it is published to. Credentials belong in [Application Credentials](/merge-agent-handler/administer/application-credentials).
* **Do not use a skill to grant access.** Naming a connector does not connect it. If an agent lacks a tool, fix the Tool Pack.
* **Do not write one skill for six unrelated jobs.** Retrieval is all or nothing, so a combined skill spends context on five procedures the agent did not need.
* **Do not restate the tool schema.** Agent Handler already gives the agent each tool's parameters. Spend the space on judgment the schema cannot carry.
* **Do not describe your org chart in prose.** Escalation paths and approval thresholds are tables, and usually belong in a bundled reference file.

Next: [Publishing skills](/merge-agent-handler/build/skills/publishing-skills)