How-to › Expose your API to agents

How to write a skill description that triggers at the right time#

Write the description around the words users type and the files they edit, then score it on ten fixed prompts beside a Cursor rule and a Copilot path glob.

Audience
Agent builder
Level
intermediate
Topic
Write agent skills and instruction files
Verified

Your release skill never loads when someone asks for release notes. Its description names the procedure the way the team does, Lantern, a word no user types in a prompt. The agent scans the listing, sees nothing that matches changelog or release, and writes the notes from scratch, differently each time. The one file that would have made the output consistent sat unread.

What you get

You will end up with a description built from the words users type and a ten-prompt check that counts misses and false fires. The same rule appears as a Cursor .mdc and a Copilot applyTo glob. This is for you if your team shares skills.

Short answer

Write the description in the third person around what the skill does and the words a user types: the task nouns, the file names, and the commands. Keep the trigger sentence first, because Claude Code’s listing shows at most 1,536 characters of it. Then score it on ten prompts, five that should load the skill and five that should not, and count misses and false fires separately. A glob on applyTo or globs lets the path decide instead.

You will need

Node 22 or later, a skill or rule you own, and ten prompts from real sessions. Verified 2026-10-21 against Node 22.22.0. The skill follows the Agent Skills specification, which caps a description at 1,024 characters and asks it to say what the skill does and when to use it.1 The sample runs offline and calls no model: the description check is a lexical proxy, and the glob check is exact.

Approaches compared

ApproachWhen it fitsWhat it costs youWhen to pick something else
A Copilot instructions file with applyToRules about a kind of file, where the path says enoughA rule that loads only when a matching file is in the request, so a task phrased without a file never sees itThe trigger is in what the user asked, not in which file is open
A Cursor .mdc rule with a description and alwaysApply falseCursor users, and a rule the agent should pull in when it judges it relevantA Cursor-only format, and a description the agent reads on every turn to decide, the same cost as a skillThe team uses more than one agent, where the skill format is the portable one
The SKILL.md description fieldAny agent that reads the Agent Skills format, and a procedure users ask for in wordsA listing entry on every turn, a cut at 1,536 characters in Claude Code, and a miss whenever the words differ from yoursThe rule must hold whenever a file is touched, whether or not anyone asked

The glob and the description answer different questions. A glob fires when a matching file is in the request, so it cannot see a prompt with no file open, and it cannot tell an edit to CHANGELOG.md from a release. A description is read by the model on every turn, so it fires on intent and costs a listing entry per skill per turn.

Write the description from the prompts, not from the procedure

The skill is a release procedure the team calls Lantern, and its first description said so.

---
name: release-notes
description: Lantern, the Northwind shipping procedure. Use when running Lantern.
---

No word in that sentence is one a user types. The rewrite starts from the prompts: what the skill does, then the nouns people use when they want it, then the file it edits.

---
name: release-notes
description: Drafts changelog entries and release notes from conventional commits, and bumps the version. Use when the user asks for a changelog, release notes, a version bump, or a release, or edits CHANGELOG.md.
---

Anthropic’s skill authoring guidance asks for the third person, for both what the skill does and when to use it, and for specific key terms. The description is how a model chooses among what may be a hundred skills.2 The Claude Code skills reference adds that the combined description and when_to_use text is cut at 1,536 characters in the listing, so the key use case goes first. The body stays out of the description. An agent that has decided to load the skill reads the body anyway, and an agent that has not would pay for every word of it on every turn.

Score it on ten prompts, five that should fire and five that should not

The ten prompts are fixed, five that should load the skill and five that should not. Each carries the files that were open at the time, because the glob approaches read nothing else.

  { "text": "write the changelog entry for the 2.4 release", "files": ["CHANGELOG.md"], "expect": true },
  { "text": "draft release notes from the commits since v2.3", "files": [], "expect": true },
  { "text": "bump the api package to 2.4.0 and update the changelog", "files": ["packages/api/package.json", "CHANGELOG.md"], "expect": true },

The harness decides three ways. For a description it checks whether the prompt and the description share a content word, a crude stand-in for the model’s reading and the one property the author controls. For a glob it matches the open files with Node’s path.matchesGlob. For an always-on rule it fires.

export const byDescription = (description) => {
  const vocabulary = terms(description)
  return (prompt) => [...terms(prompt.text)].some((w) => vocabulary.has(w))
}

export const byGlob = (patterns) => {
  const globs = patterns.split(',').map((g) => g.trim()).filter(Boolean)
  return (prompt) => prompt.files.some((f) => globs.some((g) => matchesGlob(f, g)))
}

export const always = () => true

Each approach is scored on fires, misses, false fires, and the characters of context it consumed across the ten prompts. That cost is a listing entry on every turn, plus the body on every turn it fired.

export function evaluate({ decide, listing = '', body }, prompts) {
  const fired = prompts.filter(decide)
  const misses = prompts.filter((p) => p.expect && !decide(p))
  const falseFires = prompts.filter((p) => !p.expect && decide(p))
  const chars = listing.length * prompts.length + body.length * fired.length
  return { fired: fired.length, misses, falseFires, chars }
}
node compare.mjs
approach                                fires   misses  false   chars
Copilot applyTo glob                    4       2       1       4932
Cursor alwaysApply true                 10      0       5       12330
Cursor description, alwaysApply false   5       0       0       8165
SKILL.md description, internal names    0       5       0       830
SKILL.md description, user words        5       0       0       8315

Copilot applyTo glob: missed "draft release notes from the commits since v2.3" (no file in context)
Copilot applyTo glob: missed "what should go in the release notes for this pull request"
Copilot applyTo glob: fired on "add lodash as a dependency of the api package"

The internal-name description fires on nothing. The rewrite fires on the five positives only, and the Cursor .mdc carrying the same description scores the same, because the mechanism is the same and only the file format differs. The proxy is deliberately crude. It reads vocabulary, and it cannot tell write the changelog from fix the typo in the changelog, which a model can. To measure the model, the claude plugin eval command runs prompts against a plugin and grades each on whether the Skill tool was used.

Let the path decide where the file says enough

Two of the approaches never read the prompt. Copilot’s .instructions.md carries an applyTo glob and joins a request when a file it matches is part of it, as the repository instructions documentation describes.

---
applyTo: "**/CHANGELOG.md,packages/*/package.json"
---

The glob row fires on four prompts, misses two, and fires once where it should not. The misses are the prompts with no matching file open: a request for release notes with nothing open, and one asked while editing a source file. The false fire is a dependency added to package.json, which looks exactly like a version bump from the path’s point of view. A glob is exact and cheap, with no listing entry at all, but it reads the path and never the request.

Cursor puts both mechanisms in one file format and lets the frontmatter choose between them.

---
description: Drafts changelog entries and release notes from conventional commits, and bumps the version. Use when the user asks for a changelog, release notes, a version bump, or a release, or edits CHANGELOG.md.
alwaysApply: false
---

Cursor’s rules documentation reads it as a table. alwaysApply: true loads the rule on every chat. globs without a description attaches it when a matching file is in context. A description without globs, as here, hands the decision to the agent, which is the skill mechanism under another name.3 Claude Code offers the reverse inside the skill format: a paths field on SKILL.md limits automatic loading to work on matching files.

Check it worked

The third test pins the glob’s behavior, because its misses and its false fire are the ones a reader is most likely to doubt.

test('a glob misses the prompts with no matching file in context and fires on an edit to a matching file', () => {
  const r = row('Copilot applyTo glob')
  assert.deepEqual(r.misses.map((p) => p.text), [
    'draft release notes from the commits since v2.3',
    'what should go in the release notes for this pull request',
  ])
  assert.deepEqual(r.falseFires.map((p) => p.text), ['add lodash as a dependency of the api package'])
})
node --test trigger.test.mjs
1..7
# tests 7
# suites 0
# pass 7
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 163.348006

The other tests pin the internal-name description at zero fires, the rewrite at zero misses and zero false fires, and the always-on rule at 5 false fires.

When it goes wrong

The trigger sentence is in the description, but the skill never loads. The description explains the procedure’s history first and runs to 1,624 characters, over the specification’s limit of 1,024. Claude Code shows the first 1,536 characters of an entry in its listing, so the sentence that says when to use the skill is the one that gets cut.4

node pitfall.mjs
description: 1624 characters, the listing shows the first 1536
trigger sentence survives the cut: false
full description: fires 5, misses 0, false fires 0
what the listing shows: fires 0, misses 5, false fires 0

The same words in a different order go from five hits to none. Put the use case in the first sentence, the history nowhere, and the whole description inside the specification’s 1,024 characters.

The skill loads on every turn. The rule was marked alwaysApply: true, or its description claims every task in the repository. The comparison prices that: five false fires, and 12,330 characters of context across ten prompts against 8,315 for the description that fires on half of them. Those characters sit where the code the agent was asked to read would otherwise be.

The frontmatter does not parse. Claude Code then loads the skill with no fields set, and the listing falls back to the body’s first line, # Release notes, for a description. /release-notes still works, so the failure looks like a wording problem, but it is a YAML one. A colon followed by a space inside an unquoted description breaks it; quote the value.

When not to do this

Do not write the description for the people who built the skill. They know what Lantern is; the agent is reading a stranger’s prompt. Every noun in the description should be one that appeared in a real request.

Do not reach for alwaysApply: true because a rule matters. A rule that loads on every turn costs its length on every turn, and a rule that must hold belongs in a hook that runs whether or not the agent agrees.

Do not use a glob for a procedure users ask for in words. Release notes are requested, not edited into being, and a glob on CHANGELOG.md sees the edit after the fact.

Do not treat the lexical check as the measurement. It tells you whether the words are there. Whether the model loads the skill is a question for a run of the agent with the skill in place and the same ten prompts, counted the same way.

Last verified

Verified 2026-10-21 against Node 22.22.0. Every output block is what the command preceding it printed. The description check is a lexical proxy over a fixed prompt set, not a model’s decision, and no agent was run; the glob matching uses Node’s own matcher and is exact. Each tool’s loading rule is taken from its documentation.

Footnotes

  1. The specification counts in three units. A name is at most 64 characters and a description at most 1,024. The body of SKILL.md should stay under 5,000 tokens and the file under 500 lines. The name and description of every installed skill, budgeted at about 100 tokens together, are loaded at startup for all of them. Whichever unit the reader counts in, one of the limits is written in it. ↩︎ Back to text

  2. The authoring guidance gives a reason for the third person that has nothing to do with style. The description is injected into the system prompt, and an inconsistent point of view can cause discovery problems. It also publishes a list of descriptions to avoid, among them Helps with documents, Processes data, and Does stuff with files, a list that reads as collected rather than invented. ↩︎ Back to text

  3. Cursor’s rules page defines a project rule by its frontmatter. A plain .md file in .cursor/rules is ignored because it has no description, globs, or alwaysApply to read, and the page points anyone who wants plain markdown at AGENTS.md instead. A rule, in other words, is a file whose first lines say when it applies; the rest is content. ↩︎ Back to text

  4. The Claude Code skills reference gives the listing a budget of 1% of the model’s context window. When the entries overflow it, the listing drops descriptions starting with the skills invoked least, so the skills in use keep their text. Each entry is capped at 1,536 characters regardless. Both numbers are settings, skillListingBudgetFraction and skillListingMaxDescChars, so both were chosen rather than derived. ↩︎ Back to text

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Listen to Fireside, the Voxgig podcast

Since 2018, Richard Rodger has talked with developer advocates, event organizers, and the people who build developer-facing companies about the specifics of the work.