Skip to content

Agents and skills

Two things an extension can contribute, often confused, worth keeping apart.

An agent is a worker: a model, a system prompt, a tool set, and limits. It is selected — by the user, or by another agent delegating to it — and it runs a turn.

A skill is knowledge: a name, a one-line trigger description, and a body of instructions. Every skill’s name sits in the system prompt permanently; the body is loaded only when the model decides it needs it.

The distinction that matters in practice: an agent answers who is doing this, a skill answers how it is done. Neither needs a permission, and neither needs any JavaScript — an extension that is nothing but a manifest and two markdown files is a working extension.

You want to… Ship
A reviewer with a narrow tool set and a strict prompt An agent
A researcher on a cheaper, faster model An agent
Work that runs on a schedule without a user An agent with a trigger
A procedure any agent might need occasionally A skill
Your product’s conventions, so the model follows them when relevant A skill
Knowledge one specific agent should always have An agent whose skills: names it

The cost asymmetry is the deciding factor. An agent’s prompt is paid on every turn that agent runs. A skill’s description is paid on every turn of every agent; its body is paid only on the turns that load it. Long content that is only sometimes relevant belongs in a skill body.

Drop a markdown file in agents/. No manifest entry, no build step.

agents/release-notes.md
---
name: Release Notes Writer
description: Turns a commit range into user-facing release notes. Use when preparing a changelog or a release announcement.
model: balanced
tools: [file, terminal]
deny_tools: [write_file, edit_file]
max_turns: 20
icon: scroll-text
color_token: blue
---
You write release notes for humans, not for other engineers.
Read the commit range you are given with `git log`, group changes by what a
user would notice, and drop anything with no user-visible effect.
Output exactly three sections: **Added**, **Fixed**, **Changed**. One line per
item, present tense, no commit hashes, no author names. If a section would be
empty, omit it.

Everything below the frontmatter is the system prompt. The agent id is the filenameagents/release-notes.md is the agent release-notes — and name is only the display label. The directory is scanned one level deep, .md only.

Key Type Default Notes
name string Required. A file without it is skipped with a warning.
description string '' Shown in pickers, and to other agents deciding whether to delegate here. Write it as a trigger.
model string inherits Either a tier — fast, balanced, powerful — or a specific model id.
tools string[] every registered tool Mixed list of tool categories and exact tool names.
deny_tools string[] Removed after categories expand. Applied last, so it always wins.
skills string[] Skill names whose bodies are inlined into this agent’s prompt on every run.
max_turns number 25 Ceiling on loop iterations.
max_tokens number model default Output cap per turn, clamped to the model’s maximum.
thinking none|low|medium|high Anything else warns and is dropped.
budget mapping { turns?, cost_usd?, time_minutes?, mode? }, mode is soft (default) or hard.
icon string A Lucide icon name.
color_token string blue, purple, emerald, pink, gray, indigo, amber, rose.
triggers array Scheduled or webhook invocation. See Scheduled work.

An unrecognized key is dropped with a warning, and common wrong names get a “did you mean” — max_iterations and max_steps point at max_turns, color at color_token, allowed_tools at tools, and system_prompt or prompt at “the markdown body below the frontmatter”, which is where the prompt actually goes.

model: balanced is usually right. The three tiers resolve through user-editable slots, and the balanced slot defaults to following the chat model — so a balanced agent tracks whatever the user has chosen, while fast and powerful deliberately do not.

Naming a specific model id pins it. If that id is not in the catalogue, the host warns and falls back to the tier, or to the default agent slot. Pinning is worth it only when the agent genuinely depends on one model’s behavior.

Each entry in tools is resolved as a category if the platform knows that category name, and as an exact tool name otherwise. There is no glob syntax.

The categories: file, terminal, analysis, memory, delegation, mcp, workflow, meta, docs, media, search, testing, storage — plus any category an extension has introduced.

tools: [file, search, crm_create_contact] # two categories and one specific tool
deny_tools: [write_file, edit_file] # subtract the writes back out

Three behaviors to know. Omitting tools entirely — or giving an empty array — means every registered tool. Any agent that does narrow tools automatically gets use_skill added, so it can still load skills. And a misspelled entry is neither a category nor a tool, so it silently contributes nothing; the host logs one validation line per agent naming the unresolvable entry, which is where to look when an agent seems to be missing a tool.

The manifest alternative, and why to skip it

Section titled “The manifest alternative, and why to skip it”

contributes.agents can declare an agent inline:

{ "contributes": { "agents": [{ "id": "data-analyst", "name": "Data Analyst", "tools": ["analyze_data"] }] } }

It is a much weaker shape. There is nowhere to put a prompt, so the agent has none; the id is sanitized to lowercase and hyphens; file and terminal are added to whatever tools you listed; max_turns is fixed at 25; and the model is whatever the default agent slot resolved to at load time. Use it only for an agent that genuinely needs no prompt. Otherwise ship the markdown file.

Agents live in four scopes, in increasing precedence: built-in, extension, user, workspace. Two agents with the same id in the same scope is an error — a second extension registration throws, naming the extension that got there first. The same id in a higher scope is a legitimate override, and it is a full replacement rather than a field-by-field merge: the winning definition’s prompt, tools, and model are used, and nothing is inherited from the one it shadows.

So pick ids a user is unlikely to collide with, and expect that a user can deliberately shadow your agent with one of their own.

Three real paths:

  • The user launches it. The Library lists every agent, including yours, and running one opens a new chat bound to that agent. Every turn in that chat goes to it.
  • Another agent delegates to it. The default agent can spawn a sub-agent in an isolated context, choosing from a catalogue of agent ids and descriptions injected into its prompt. Your description is the entire basis for that choice, so write it as “use when…”, not as a title.
  • A trigger fires it. A triggers block runs the agent on a schedule or a webhook without a user present.

Typing @release-notes in the composer autocompletes from the same list, but be clear on what it does: it inserts the text of the mention into your message. The bound agent reads it and may delegate accordingly. It is a way to refer to an agent, not a switch that rebinds the turn.

An extension can also start an agent run itself with ctx.api.agents.delegate, and list what is available with pluginAPI.agents.list().

Two layouts. A single file, when the skill is only prose:

skills/invoice-conventions.md
---
name: invoice-conventions
description: How this product numbers, dates, and rounds invoices. Use before creating, editing, or explaining any invoice.
---
# Invoice conventions
## Numbering
`INV-{YYYY}-{sequence}`, sequence padded to four digits, never reused, never
reset mid-year. A credit note uses the same sequence with a `CN-` prefix.
## Dates
`issuedAt` is the date the invoice was sent, not the date it was created. A
draft has no `issuedAt` at all — leave it null rather than guessing.
## Rounding
Round each line to two decimals, then sum. Never sum then round; it produces
totals that disagree with the printed lines by a cent.

Or a directory, when the skill carries files with it:

skills/
release-process/
SKILL.md
references/
hotfix.md
rollback.md
scripts/
check-tags.sh

The file must be named exactly SKILL.md. A subdirectory without one is ignored silently. Only references/, scripts/, and assets/ are scanned for bundled resources, one level deep, files only.

Declare the directory in the manifest:

{ "contributes": { "skills": true } }

true scans skills/. A string names a different directory. An array names several.

name is required — a skill without one is skipped, and the warning says so explicitly, because an unnamed skill has nothing to be invoked by. id is accepted as an alias for name. description is optional but is the whole trigger mechanism, so treat it as required.

Be aware that the other frontmatter keys you may see in skill files elsewhere — version, tags, disable-model-invocation — currently have no effect on a skill contributed by an extension. They are read for skills the user or workspace provides, not for yours. Do not tune them expecting a result.

Two layers, and understanding them is what makes a skill get used.

The listing. Every skill’s name appears in the system prompt under “Available Skills” with its description, permanently, for every agent. Names are never dropped — the listing has a token budget, and under pressure descriptions are shortened rather than skills being omitted, because a skill the model cannot see does not exist.

The load. When the model decides a skill applies, it calls use_skill("invoice-conventions") and gets the body back. The call is stateless: there is no activation, no unload, and no “currently active skill” — each call just returns text. Bodies are never truncated, so a long body is returned whole, and it is never dropped during context compaction while the model is following it.

For a directory skill, the response ends with the list of bundled resources, and the model pulls one with a second call:

use_skill("release-process", "references/hotfix.md")

There is also find_skill(query) for searching by intent when the listing is not enough, and an agent can pre-load skill bodies into its prompt by naming them in its skills: frontmatter — useful when the knowledge is not optional for that worker.

The description is a trigger, not a title. The model reads it to decide whether to spend a tool call. Say the situation, not the subject.

# Weak — a topic. The model has no idea when this applies.
description: Information about invoices.
# Strong — a situation with verbs.
description: How this product numbers, dates, and rounds invoices. Use before
creating, editing, or explaining any invoice.

Then, for the body:

  • Write a procedure, not an essay. Steps, rules, tables, and the specific strings and formats to use. The model is going to act on this immediately.
  • One skill, one job. A skill that covers three unrelated areas will be loaded for one of them and waste context on the other two. Split it.
  • Say the non-obvious thing. The model already knows how to write TypeScript. It does not know your numbering scheme, your rounding rule, or which of two plausible approaches your codebase has settled on.
  • Put long detail in references/. Keep SKILL.md as the map — when to use which resource — and let the model pull only the branch it needs.
  • Include the failure mode. “If X, the symptom is Y” saves more turns than any amount of correct-path prose.

Never point a skill at a path in another repository

Section titled “Never point a skill at a path in another repository”

This is the rule that costs the most when broken, and it looks harmless.

<!-- Wrong. Do not do this. -->
For the full type definitions, read
`packages/extension-sdk/types/wamp-extension-sdk.d.ts`.

The skill ships inside your extension, but the model’s working directory is the user’s project. A repository-relative path in a skill body is an instruction to go looking, and the model will: it globs the disk, finds some checkout of something with a similar-looking path, reads a stale copy, and proceeds confidently from the wrong information. In the incident that produced this rule, that pattern burned roughly fifteen turns before anyone noticed the file being read was not the file that shipped.

Three correct alternatives, in order of preference:

  1. Inline it. If the model needs the content, put the content in the body.
  2. Bundle it. Ship it under references/ and reference it the only way that resolves — use_skill("<name>", "references/types.md"), which reads from inside your extension and cannot pick up a stray copy.
  3. Name it without pointing at it. If the model does not need to read a file but does need to know it exists, say so and say explicitly not to open it: “the platform typechecks every save, so trust the error rather than opening the declaration files.”

The same applies to absolute paths, ~-relative paths, and repository URLs. If the body contains a path, ask what happens when that path exists on the user’s machine and contains something else.

Skills merge across five sources — built-in, user, extension, installed, and workspace — keyed on name, with later sources winning. A workspace skill named invoice-conventions shadows yours entirely. Prefix names that are likely to clash.

One agent and one skill, no main, no build.

extension.json
{
"name": "Invoice Assistant",
"version": "1.0.0",
"engines": { "wamp": "^12.0.0" },
"description": "An invoice reviewer agent plus this product's invoice conventions.",
"permissions": [],
"contributes": { "skills": true }
}
agents/invoice-reviewer.md
---
name: Invoice Reviewer
description: Checks a draft invoice against this product's conventions and reports problems. Use before an invoice is sent.
model: balanced
tools: [file]
skills: [invoice-conventions]
max_turns: 15
icon: receipt
color_token: amber
---
You review draft invoices and report problems. You do not fix them.
For each invoice you are given, check the number format, the issue date, the
per-line rounding, and the total. Report every problem you find as a single
line: what is wrong, what it should be, and which line it is on. If everything
is correct, say so in one sentence and stop.
Never edit a file. Never send anything.

skills/invoice-conventions.md is the file shown earlier, with one section added at the end — the kind of content that saves the most turns:

## Common failure
A total off by exactly one or two cents is almost always sum-then-round. Check
the line values before looking anywhere else.

The agent names the skill in skills:, so its body is in the prompt on every run — the knowledge is not optional for this worker. The skill is also in the general listing, so any other agent can load it when an invoice comes up.