Composite Skills

Composite skills combine related skills into a single artifact with shared references. A composite is a regular skill() that has sub-skills and/or topics registered on it. The dispatcher state machine classifies intent and routes to the appropriate sub-skill or resolves a reference topic directly.

Use composites when you have multiple skills that overlap in references, params, or user-facing scope — for example, a contentful-doctor, contentful-setup, and contentful-faq that all reference the same Contentful documentation.

Registering sub-skills

Sub-skills are standalone SkillDefinitions registered with .subskill():

import { skill, type } from '@contentful/skill-kit';
import doctorSkill from './subskills/doctor.js';
import setupSkill from './subskills/setup.js';

export default skill({
  name: 'contentful-help',
  entry: 'classify',
  params: type({ query: 'string' }),
})
  .step('classify', {
    prompt: ({ params }) => `Classify intent: "${params.query}"`,
    response: type({ intent: 'string', confidence: 'number' }),
    next: ({ response }) => {
      if (response.confidence < 0.7) return 'clarify';
      if (response.intent === 'doctor') return 'get-space';
      return `subskill:${response.intent}`;
    },
  })
  .step('get-space', {
    prompt: 'Ask the user for their Contentful space ID.',
    response: type({ spaceId: 'string' }),
    next: 'subskill:doctor',
  })
  .subskill('doctor', doctorSkill, {
    params: (_response, store) => ({ spaceId: store.steps['get-space']?.spaceId ?? '' }),
  })
  .subskill('setup', setupSkill)
  .build();

Each .subskill() registration accepts an optional params function that maps the dispatcher’s state (step response + store) to the sub-skill’s params.

Registering topics

Topics are reference entries — the same as on reference() skills:

.topic('rate-limits', {
  label: 'API rate limits',
  content: ({ refs }) => refs.load('rate-limits.md'),
})

Topics can be resolved as dispatch targets (next: 'topic:rate-limits') or looked up directly via CLI (scripts/run topic rate-limits).

Routing

The dispatcher is a full state machine. Any step’s next can return:

The dispatcher can have as many steps as needed before routing — classify, triage, gather context, ask clarifications. It’s not limited to a single classification step.

Step name namespacing

Sub-skill steps are prefixed at the protocol layer:

The engines never see prefixes — the composite entry handles this automatically.

CLI protocol

All commands go through scripts/run. Session mode is recommended:

# Session mode (recommended)
scripts/run --params '{"query":"my types are broken"}' --session new
# -> {"sessionId":"abc123","file":"/tmp/skill-kit-abc123.jsonl","line":2}
scripts/run advance --session abc123
# -> 4  (line number to read)

# Stateless mode (fallback)
scripts/run --params '{"query":"my types are broken"}'
scripts/run advance --step classify --output '{"intent":"doctor","confidence":0.9}' --history '[...]'

# Direct sub-skill access (bypasses dispatcher)
scripts/run doctor --params '{"spaceId":"abc123"}' --session new

# Reference topics
scripts/run topics
scripts/run topic rate-limits

One session spans the entire composite workflow — dispatcher and subskill steps are tracked in the same JSONL file.

Testing composites

Use runComposite from @contentful/skill-kit/test:

import { runComposite, mockModel } from '@contentful/skill-kit/test';
import skill from './skill.js';

const result = await runComposite(skill, {
  params: { query: 'my entries are broken' },
  refs,
  model: mockModel({
    classify: { intent: 'doctor', confidence: 0.9 },
    'get-space': { spaceId: 'abc123' },
    'doctor/diagnose': { issues: ['broken refs'], healthy: false },
    'doctor/report-issues': { summary: 'Found 1 issue.' },
  }),
});

assert.equal(result.redirectedTo?.name, 'doctor');
assert.deepEqual(result.path, ['classify', 'get-space', 'doctor/diagnose', 'doctor/report-issues']);

Use directSubskill: 'doctor' to test a sub-skill in isolation, bypassing the dispatcher.

Difference from modules

ModulesSub-skills
NamespaceFlattened into parentPrefixed: doctor/diagnose
StoreMerged with parentIsolated per sub-skill
ParamsCannot access parentReceives mapped params
Entry pointNo independent invocationCLI subcommand
TestingWithin host skillStandalone with runSkill()

Modules are for reusable step groups within a single skill. Sub-skills are for independent workflows bundled into one artifact.

Constraints