MCPcopy Index your code
hub / github.com/contentful/skill-kit

github.com/contentful/skill-kit @v1.10.1

Chat with this repo
repository ↗ · DeepWiki ↗ · release v1.10.1 ↗ · + Follow
410 symbols 1,151 edges 142 files 10 documented · 2%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README
<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%">

TypeScript 5.9+ Node.js 24+ ArkType 4

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.

Quick Start

pnpm add @contentful/skill-kit

Define a skill

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();

Test without an agent

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!' }

Build a distributable skill

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.

Skill Types

Workflow skills

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.

Reference skills

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.

Composite skills

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.


How It Works

+---------+  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"]
    }
  }
}

Host-aware primitives

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.

Step Lifecycle

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
  1. prompt -- the CLI emits the step's prompt and schema to the agent (steps without a prompt auto-advance immediately)
  2. model -- the agent reads the prompt, does the work, returns structured output
  3. validate(response) -- the CLI validates the output against the step's ArkType schema (skipped for response-less steps)
  4. action.mapInput -- if the step has an action, action.mapInput transforms the validated response into action input
  5. action.run -- the action executes CLI-side (file writes, API calls, etc.)
  6. save -- the save 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.
  7. store -- the step result is appended to store.steps, and sub-store writes are merged into their top-level store properties
  8. next -- the transition function determines the next step (or terminal)

The store knows your workflow graph

The 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.


Examples

get-to-know-you (workflow)

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' },
  ],
})

Full source

ts-patterns (reference)

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'] },
  ),
})

Full source

contentful-help (composite)

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.

Full source


API

Workflow Builder

```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 }) //

Extension points exported contracts — how you extend this code

McpSession (Interface)
(no doc) [9 implementers]
src/protocol/mcp-session.ts
InlineActionContext (Interface)
* Context passed to inline action functions.
src/skill-builder.ts
BranchEntry (Interface)
* Shape matcher for branch entries in ExtractBranchTargets. * * Uses `any` for the `when` callback parameters because
src/types/store.ts
StepDefinition (Interface)
(no doc) [1 implementers]
src/types.ts
ModuleConfig (Interface)
(no doc)
src/module.ts
LintDiagnostic (Interface)
(no doc)
src/lint/types.ts
CycleGuardResult (Interface)
(no doc)
src/validation/cycle-guard.ts
ChecklistItem (Interface)
(no doc)
src/render/checklist.ts

Core symbols most depended-on inside this repo

step
called by 498
src/module.ts
skill
called by 208
src/skill.ts
build
called by 147
src/module.ts
emit
called by 144
scripts/stress-test-gen.ts
advance
called by 104
src/protocol/mcp-session.ts
start
called by 86
src/protocol/skill-engine.ts
buildAccessor
called by 38
src/runtime/state-store.ts
action
called by 30
src/action.ts

Shape

Function 197
Method 102
Interface 83
Class 28

Languages

TypeScript100%

Modules by API surface

src/types.ts56 symbols
src/protocol/session.ts22 symbols
docs-site/src/components/HeroDemo.tsx21 symbols
src/runtime/engine.ts14 symbols
src/protocol/subskill-engine.ts14 symbols
src/runtime/state-store.ts13 symbols
src/protocol/mcp-session.ts12 symbols
src/skill-builder.ts11 symbols
scripts/update-licenses.mjs11 symbols
src/protocol/invocation-context.ts10 symbols
docs-site/src/data/storyboard.ts10 symbols
src/validation/cycle-guard.ts9 symbols

For agents

$ claude mcp add skill-kit \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact