<img src="https://github.com/contentful/skill-kit/raw/v1.10.1/assets/banner-light.png" alt="@contentful/skill-kit — TypeScript SDK for agent skills: workflow state machines and progressive-disclosure references." width="100%">
Quick Start · Skill Types · How It Works · Examples · API · Full API Reference · Architecture · Spec
A prose skill is a blob of markdown the agent reads all at once. That works until it doesn't — multi-step workflows need branching and validation, and large reference docs need progressive disclosure.
skill-kit gives you two tools. Workflow skills are typed state machines — steps with prompts, ArkType schemas, and explicit transitions. Reference skills are on-demand topic loaders — the agent reads the SKILL.md, then loads detailed content one topic at a time. Both bundle into self-contained packages that agents invoke via Bash.
import { skill, type, terminal } from '@contentful/skill-kit';
export default skill({
name: 'repo-doctor',
entry: 'diagnose',
})
.step('diagnose', {
prompt: 'Inspect the repository. Report health checks for CI, linting, and test coverage.',
response: type({
checks: {
name: 'string',
status: "'pass' | 'fail'",
detail: 'string',
}[],
}),
next: [
{ to: 'remediate', when: ({ response }) => response.checks.some((c) => c.status === 'fail') },
{ to: 'report' },
],
})
.step('remediate', {
/* fix failing checks */
})
.step('report', {
/* render results */
next: terminal,
})
.build();
That's a skill. The agent sees one step at a time, returns structured output, and the CLI decides what happens next.
pnpm add @contentful/skill-kit
import { skill, type, terminal } from '@contentful/skill-kit';
export default skill({
name: 'greet',
entry: 'ask',
})
.step('ask', {
prompt: 'Ask the user for their name.',
response: type({ name: 'string' }),
next: 'welcome',
})
.step('welcome', {
prompt: ({ store }) => `Say hello to ${store.steps.ask.name}.`,
response: type({ message: 'string' }),
next: terminal,
})
.build();
import { runSkill, mockModel } from '@contentful/skill-kit/test';
import greet from './skill.ts';
const result = await runSkill(greet, {
model: mockModel({
ask: { name: 'Alice' },
welcome: { message: 'Hello, Alice!' },
}),
});
// result.path → ['ask', 'welcome']
// result.response → { message: 'Hello, Alice!' }
npx skill-kit build src/skill.ts -o skill --mode node # lightweight Node.js bundle
npx skill-kit build src/skill.ts -o skill # standalone executables (default)
Output is an agentskills.io-compliant directory:
skill/
SKILL.md <- Generated. Agents read this.
package.json
scripts/
run <- Shell wrapper. The public interface.
bin/
greet.mjs <- Node mode: single bundle (~100-500KB)
# -- OR for default bun mode: --
# greet-darwin-arm64 <- Standalone executables (~50-100MB each)
# greet-linux-x64
Install it anywhere — skills add, agents-kit install, or just git clone.
Typed state machines with steps, ArkType schemas, branching, and the store for cross-step state. The hero example above shows the pattern — skill() -> .step() -> .build(). Add params for immutable input, store for typed access to prior step results, askUser for interactive questions, and action for side effects. The store knows your workflow graph — guaranteed steps are non-optional, branch targets require ?.. Steps without a prompt auto-advance (useful for computation-only routing steps); steps without a response schema skip validation. See the full API reference for all options.
For skills that don't need a workflow — just progressive disclosure of content:
import { reference, render } from '@contentful/skill-kit';
export default reference({
name: 'api-guide',
description: 'API reference for the Foo service.',
})
.topic('auth', {
label: 'Authentication and token management',
content: ({ refs }) => refs.load('auth.md'),
})
.topic('errors', {
label: 'Error codes and troubleshooting',
content: () => render.table(ERROR_CODES, { columns: ['code', 'meaning', 'fix'] }),
})
.build();
The generated SKILL.md lists topics. Agents load them on demand:
scripts/run topics # list all topics
scripts/run topic auth # load a specific topic -> plain text to stdout
No JSON, no history, no state machine. Just topic <name> -> text.
When related skills share references and overlap in scope, combine them into a single composite. A composite is a regular skill() with sub-skills and topics registered on it — a dispatcher state machine that routes to independent sub-skill workflows or resolves reference topics directly.
import doctorSkill from './subskills/doctor.js';
import setupSkill from './subskills/setup.js';
skill({ name: 'contentful-help', entry: 'choose', system: 'You are a helpful Contentful support assistant.' })
.step('choose', {
prompt: act.askUser({ type: 'structured', question: 'What do you need?', options: [...] }),
response: type({ choice: 'string' }),
next: ({ response }) => `subskill:${response.choice}`,
})
.topic('rate-limits', { label: 'API rate limits', content: ({ refs }) => refs.load('rate-limits.md') })
.subskill('doctor', doctorSkill, { params: (_out, store) => ({ spaceId: store.steps.choose.spaceId }) })
.subskill('setup', setupSkill)
.build();
Sub-skills are standalone skill().build() definitions — testable independently. next returns 'subskill:<name>' or 'topic:<name>' to route. See the Composite Skills guide.
+---------+ scripts/run +-------------+
| | --------------------> | |
| Agent | < JSON: prompt, | Skill CLI |
| | schema | (bundled) |
| | | |
| | scripts/run advance | |
| | --------------------> | |
| | < JSON: next prompt | |
| | ...or done | |
+---------+ +-------------+
The SDK supports three invocation modes. MCP mode (preferred) runs the skill as a long-lived MCP stdio server — the agent interacts through start/advance tool calls with no Bash or file I/O visible to the user. Session mode writes protocol data to a JSONL temp file — the agent reads/writes the file instead of parsing verbose JSON from stdout. Stateless mode passes the full conversation history via --history on every advance call. All modes share the same engine, validate against ArkType schemas, and return the next step's prompt.
To use MCP mode, configure the skill as an MCP server in your agent host:
{
"mcpServers": {
"my-skill": {
"command": "/path/to/skill/scripts/run",
"args": ["mcp", "--host", "claude-code"]
}
}
}
The SDK renders primitive directives as XML tags. The preamble (sent on first response) maps each tag to the host's tool via a markdown table:
| Tag | Description | Example tool (Claude Code) |
|---|---|---|
<ask-user> |
Structured or open question | AskUserQuestion |
<confirm> |
Binary yes/no confirmation | AskUserQuestion |
<plan> |
Plan presentation with steps | EnterPlanMode |
<checklist> |
Tracked task list | TaskCreate |
<survey> |
Batch multiple structured questions | AskUserQuestion |
<subagent> |
Sub-agent delegation (no-recurse guard) |
Agent |
No tool names in the XML. The preamble handles the mapping. Same skill, same XML, every host. See the architecture doc for the full tag table and how the preamble is generated.
Each step follows a fixed lifecycle. Understanding the order matters when using actions and the save callback:
prompt -> model -> validate(response) -> action.mapInput -> action.run -> save -> store -> next
action.mapInput transforms the validated response into action inputsave callback returns { step?, ...subStoreWrites }. The step property controls what gets stored as the step result (defaults to action output or response). Additional keys are deep-merged into the corresponding sub-stores.store.steps, and sub-store writes are merged into their top-level store propertiesThe store organizes state into two namespaces: store.steps for step-keyed results and top-level sub-stores for domain-structured state. Step results flow into store.steps automatically. Sub-stores are populated by save callbacks that return additional keys alongside the optional step property.
The type system tracks which steps are guaranteed (on all paths from entry) vs optional (branch targets), computed automatically from your step declarations:
.step('profile-card', {
prompt: ({ store }) => {
const name = store.steps.greet.name; // guaranteed -- non-optional
const role = store.steps['ask-role'].role; // guaranteed -- non-optional
const stack = store.steps['ask-stack']?.answer; // branch target -- optional, use ?.
const hobbies = store.steps.all('ask-hobby'); // loop visits -- typed array
const env = store.environment; // sub-store -- domain state
// ...
},
})
Zero boilerplate. No updateStash, no manual wiring. Retry loops (backward edges) don't create false branches — the forward path is still guaranteed.
A playful interview that builds a developer trading card. Shows declarative branching with NextBranch[], askUser, confirm, render helpers, actions, loop guards, and the store.
.step('ask-role', {
prompt: act.askUser({
type: 'structured',
question: "What's your primary role?",
options: [
{ value: 'dev', label: 'Developer' },
{ value: 'designer', label: 'Designer' },
{ value: 'manager', label: 'Manager' },
],
}),
response: type({ role: "'dev' | 'designer' | 'manager' | 'other'" }),
next: [
{ to: 'ask-stack', when: ({ response }) => response.role === 'dev' },
{ to: 'ask-tools', when: ({ response }) => response.role === 'designer' },
{ to: 'ask-team-size', when: ({ response }) => response.role === 'manager' },
{ to: 'ask-specialty' },
],
})
TypeScript patterns reference with on-demand topic loading. Shows render.table, render.code, and external markdown via refs.load().
.topic('error-handling', {
label: 'Error handling — Result types, custom errors, exhaustive matching',
content: () => render.table(
[
{ pattern: 'try/catch', use: 'External APIs, I/O', note: 'Catch specific error types' },
{ pattern: 'Result<T, E>', use: 'Domain logic', note: 'Forces caller to handle both paths' },
],
{ columns: ['pattern', 'use', 'note'] },
),
})
A composite skill that dispatches to doctor and setup sub-skills, or resolves FAQ topics directly. Shows .subskill(), .topic(), subskill: / topic: routing, actions for deterministic env checks, and runComposite for testing.
```typescript import { skill, type, act, view, terminal } from '@contentful/skill-kit';
skill({ name, entry, system?, version?, resolveVersion?, package?, description?, triggers?, params?, observers?, finalOutput? }) .step(name, config) // inline step -- params/store types inferred .extend(name, sharedStep, overrides) // shared step with typed overrides .register(module, { next }) //
$ claude mcp add skill-kit \
-- python -m otcore.mcp_server <graph>