Skills

Anatomy of a skill

A skill is two files. A typed manifest entry in src/lib/skills/registry.ts and a SKILL.md file on disk. The manifest controls runtime behavior. The SKILL.md carries the system prompt. The frontmatter inside the SKILL.md duplicates the manifest so the file is self-documenting for humans, with the manifest winning at load time.

Updated today

Overview

At a glance
Manifest
src/lib/skills/registry.ts
Types
src/lib/skills/types.ts
Prompt files
src/lib/skills/<category>/<id>/SKILL.md
Required fields
id, name, category, description, tools, model, maxTurns
Optional fields
author, version, license, tags
Category
One of 8 literal strings
Model
haiku, sonnet, or opus
Always-on tools
log_activity, save_memory

A skill is the smallest reusable unit the engine knows how to execute. Everything the engine needs to run one - which model to call, which tools to expose, how many tool-calling iterations to allow, and what the system prompt should be - is captured in two files that live next to each other in the source tree. There is no plugin loader, no database lookup, no runtime registration. The catalog is closed by design.

The contract is also tiny. One TypeScript interface, one filename convention, one always-on tool set. Anyone who has read this page once can scan an unfamiliar skill in under a minute and know what it does, what it touches, and what it cannot do.

SkillMeta interface

The typed contract every skill in the manifest must satisfy.

src/lib/skills/types.tsts
14export interface SkillMeta {
15 id: string // kebab-case, unique
16 name: string // human label
17 category: SkillCategory // one of 8 literal strings
18 description: string // 'Use when...' trigger blurb
19 tools: ToolName[] // allow-list of callable tools
20 model: string // 'haiku' | 'sonnet' | 'opus'
21 maxTurns: number // 3 to 12 in practice
22 author?: string // marketplace metadata
23 version?: string
24 license?: string
25 tags?: string[] // search and filtering
26}
Note
Skills extend to a full Skill type at load time. The full type adds one field, content, which holds the entire SKILL.md body that becomes the system prompt.

SkillCategory union

The category is a literal union, not a free string. Adding a category means changing the type.

src/lib/skills/types.tsts
43export type SkillCategory =
44 | "research"
45 | "lead-gen"
46 | "sales"
47 | "content"
48 | "ops"
49 | "design"
50 | "legal"
51 | "paid-ads"

The category determines two things. First, the folder a skill's SKILL.md is loaded from at runtime. Second, the group the catalog browser shows the skill under. The two stay in sync because the loader composes the file path from the category string itself.

System tools

A short list of tools every skill gets for free. They are unioned into the catalog before the integration gate runs.

src/lib/skills/types.tsts
108export const SYSTEM_TOOLS: ToolName[] = [
109 "log_activity", // emit a row to the activity sidebar
110 "save_memory", // persist a memory entry
111]
Tip
A skill manifest does not need to list these in its tools array. The engine merges them in at catalog build time. Listing them anyway is harmless; the set is deduplicated.

Manifest vs SKILL.md

The manifest is metadata. The SKILL.md is the prompt. The loader merges them, with the manifest winning the runtime fields.

src/lib/skills/registry.tsts
723export function loadSkill(id: string): Skill {
724 const meta = SKILL_MANIFEST.find((s) => s.id === id)
725 if (!meta) throw new Error(`Skill not found in manifest: ${id}`)
726
727 const filePath = path.join(SKILLS_DIR, meta.category, id, "SKILL.md")
728 const raw = fs.readFileSync(filePath, "utf-8")
729 const { frontmatter, content } = parseFrontmatter(raw)
730
731 return {
732 ...meta,
733 name: frontmatter.name || meta.name,
734 description: frontmatter.description || meta.description,
735 content, // becomes the system prompt
736 }
737}

The runtime preference order is intentional. For values that change behavior (tools, model, maxTurns, category), the manifest is the source of truth and the loader ignores whatever the frontmatter says. For human-facing strings (name, description), the frontmatter wins so a maintainer can edit the markdown file without touching the registry.

Frontmatter

Every SKILL.md opens with a YAML block that mirrors the manifest entry.

src/lib/skills/research/competitive-analysis/SKILL.mdyaml
1---
2name: competitive-analysis
3description: "Use when comparing your product against competitors, preparing for competitive deals, or building battlecards. Also use when user mentions 'competitor', 'alternative', 'vs', 'battlecard', 'win/loss', or 'competitive landscape'."
4tools: [web_search_multiple, scrape_url, search_memory, get_company_profile, save_memory]
5model: haiku
6maxTurns: 8
7category: research
8---
Warning
When the frontmatter and manifest disagree on runtime fields, the manifest wins silently. A skill that compiles in TypeScript but has stale frontmatter will still run with the manifest behavior. Keep them in sync as a discipline; CI does not enforce it.

SKILL.md body

Below the frontmatter, the markdown body becomes the system prompt. Structure it for the model, not for a human reader.

Skill bodies follow a loose template across the catalog. A short persona statement at the top, a Before Starting checklist that lists prerequisites and memory lookups, one or more numbered Mode sections describing the actual procedures, an output format spec, and a quality gate. The model reads the entire body on every invocation; long bodies mean expensive turns, so most skills stay under a thousand tokens.

Tip
The body is markdown but the model sees it as plain text. Use heading levels and lists for the model's benefit, not for visual rendering. Inline backticks help the model treat tool names and field names as literals.

Trigger description

The description field has one job. It tells the discovery step when to pick this skill.

src/lib/skills/types.tsts
20/**
21 * Trigger description - ONLY describes WHEN to use this skill.
22 * Must start with "Use when..." or similar.
23 * NEVER summarizes the workflow (Claude follows description instead
24 * of reading full skill).
25 */
26description: string

The discovery model only sees the description, not the body. If the description tries to describe what the skill does, the discovery model will start to follow the description and skip reading the full prompt. Keep descriptions to a single sentence focused on the trigger condition. Include the exact words a user is likely to type. List sibling skills the description should rule out.

Tool allow-list

The tools array on a manifest is the only place a skill declares what it can call. The engine enforces it at catalog build time.

src/lib/skills/engine.tsts
345// Union skill tools with the system tools
346const requested = Array.from(new Set([...skill.tools, ...SYSTEM_TOOLS]))
347
348// Filter out tools the user has not connected
349const connected = await fetchConnectedProviders(userId)
350const allowed = requested.filter((name) =>
351 toolPassesConnectionGate(name, connected)
352)
353
354// Serialize just the allowed schemas to the model
355const tools = getToolDefinitions(allowed)
Warning
A tool that fails the connection gate never reaches the model. That means a skill written for ten tools may run with only seven if the user has not connected the related providers. The skill body should account for this with conditional logic, or the prompt should bail out cleanly when a required tool is missing.

Model and turns

Two fields control cost and depth. Choose them together.

FieldAllowed valuesEffect
modelthe model classSelects the skill's model class, from fastest to heaviest; resolved internally to a versioned model id. Determines cost and reasoning ceiling.
maxTurns3 to 12 in practiceHard cap on the tool-calling loop. The engine forces a final answer when reached.

Pick the fastest class for tight extract-and-format work that does not require deliberation. Pick the default class for the agentic loop where the model picks tools and synthesizes results. Pick the heaviest class for long planning sessions, document scanning, or skills that have to compose more than five tool calls deep.

The turn budget is the wider lever. A six-turn budget keeps a skill snappy and predictable. A twelve-turn budget lets a skill recover from missed searches and re-plan once. Bigger than twelve is almost always a sign the skill should be split into two.

Optional fields

Four fields exist for marketplace and search use cases. None of them change runtime behavior.

FieldPurpose
authorAttribution for catalog listings and marketplace display
versionSemver string used to track skill changes over time
licenseFree-form license tag for skill distribution
tagsArray of strings used by the catalog search and filter UI

File layout

src/lib/skills
types.tsSkillMeta, SkillCategory, ToolName, SYSTEM_TOOLS
registry.tsSKILL_MANIFEST + loadSkill
engine.tsrunSkillMission, tool allow-list filter
research
competitive-analysis
SKILL.md
trend-feed
SKILL.md
lead-gen
vc-prospector
SKILL.md
email-first-touch
SKILL.md
sales
cold-email
SKILL.md
striker-post-call
SKILL.md
content
content-pulse
SKILL.md
paid-ads
ads-campaign-setup
SKILL.md
legal
contract-review
SKILL.md
ops
morning-briefing
SKILL.md
design
canvas-intelligence
SKILL.md

Glossary

SkillMeta
The typed interface every manifest entry implements. Source of truth at runtime.
Skill
SkillMeta plus the loaded content from SKILL.md. Returned by loadSkill at execution time.
SKILL.md
Markdown file holding the system prompt for one skill. Frontmatter mirrors the manifest entry.
Manifest
The exported array SKILL_MANIFEST in registry.ts. The closed catalog of all runnable skills.
Frontmatter
YAML block at the top of a SKILL.md that duplicates manifest metadata for human readers.
System tools
Two tools (log_activity, save_memory) merged into every skill's allow-list automatically.
Trigger description
The description field on SkillMeta. Used only by the discovery step to pick a skill from the catalog.
Connection gate
A pre-call filter that drops tools whose required provider is not connected for the user.