Primitives

Primitives are host-aware interactive building blocks rendered as XML tags. They let you express intent — “ask the user a question,” “present a plan for approval” — and the SDK produces XML calibrated for whichever agent host is running the skill. On Claude Code, the preamble maps <ask-user> to AskUserQuestion. On hosts without a matching tool, generic fallback instructions apply. Same skill, same XML, every host.

The CLI can only emit text. Primitives are the mechanism that makes that text reliable across hosts, and the place where prompt-engineering effort compounds across the ecosystem.

How primitives work

Primitives can be used two ways:

  1. Directly in the prompt field — pass act.askUser(...) (or any primitive) straight to prompt for steps that consist entirely of one primitive with no additional prose.
  2. act methods in prompt functions — composable directives mixed with other prompt pieces.

When a step uses a primitive, the SDK renders it as an XML tag (e.g., <ask-user>, <plan>, <checklist>). The preamble (sent once at session start) maps each tag to the host’s tools via a markdown table. The response schema is the contract regardless of host — downstream steps never know how the answer was obtained.

act.askUser() — structured and open questions

A single primitive with two modes, discriminated by type.

Structured mode

Presents fixed options via the host’s structured question tool (if available) or via a prose option list.

import { act, type } from '@contentful/skill-kit';

// Single-primitive step — passed directly to prompt
.step('choose-target', {
  prompt: act.askUser({
    type: 'structured',
    question: 'Which deployment target?',
    options: [
      { value: 'production', label: 'Production', description: 'Live, customer-facing' },
      { value: 'staging', label: 'Staging', description: 'Pre-production mirror' },
      { value: 'local', label: 'Local', description: 'Dev environment' },
    ],
  }),
  response: type({ target: "'production' | 'staging' | 'local'" }),
  next: ({ response }) => `deploy-${response.target}`,
})

// Or composed with other prompt pieces
.step('choose-target', {
  prompt: ({ act }) => [
    act.askUser({
      type: 'structured',
      question: 'Which deployment target?',
      options: [
        { value: 'production', label: 'Production', description: 'Live, customer-facing' },
        { value: 'staging', label: 'Staging' },
        { value: 'local', label: 'Local' },
      ],
    }),
    `Explain what each environment is used for before asking.`,
  ],
  response: type({ target: "'production' | 'staging' | 'local'" }),
  next: ({ response }) => `deploy-${response.target}`,
})
FieldTypeRequiredDescription
type'structured'YesDiscriminant for structured mode.
questionstringYesThe question text shown to the user.
optionsAskUserOption[]YesArray of { value, label, description?, preview?, header? }.
multiSelectbooleanNoAllow multiple selections. Defaults to single-select.

Options support optional preview and header fields on the structured config for richer presentation.

Open mode

Free-text conversation. Never uses a structured question tool — the agent asks conversationally and the user answers in their own words.

.step('ask-stack', {
  prompt: act.askUser({ type: 'open', question: "What's your go-to tech stack?" }),
  response: type({ answer: 'string' }),
  next: 'done',
})
FieldTypeRequiredDescription
type'open'YesDiscriminant for open mode.
questionstringYesThe question text.

act.confirm() — binary approval

Binary yes/no confirmation with support for destructive-operation warnings and default answers.

import { act, type } from '@contentful/skill-kit';

.step('confirm-delete', {
  prompt: act.confirm({
    message: 'This will delete 47 files in .cache/. Continue?',
    destructive: true,
    defaultAnswer: 'no',
  }),
  response: type({ approved: 'boolean' }),
  next: ({ response }) => response.approved ? 'proceed' : 'abort',
})
FieldTypeRequiredDescription
messagestringYesThe confirmation message shown to the user.
destructivebooleanNoFlags the operation as destructive. Adds warning emphasis in generated prose.
defaultAnswer'yes' | 'no'NoDefault answer on ambiguity. Defaults to 'no' if not specified.

The confirm primitive is distinct from askUser because destructive-op confirmation needs stronger defaults and warning framing that differs from a neutral question.

act.plan() — present a plan for approval

Shows a structured plan (summary + ordered steps) and asks the user whether to proceed.

import { act, type } from '@contentful/skill-kit';

.step('review-plan', {
  prompt: act.plan({
    summary: 'Migrate auth from session cookies to JWTs',
    steps: [
      'Add JWT signing and verification helpers',
      'Update login flow to issue JWTs',
      'Add compatibility layer for existing sessions',
      'Update middleware to accept both',
      'Migration script for active sessions',
    ],
  }),
  response: type({ approved: 'boolean', 'modifications?': 'string' }),
  next: ({ response }) => response.approved ? 'execute' : 'revise',
})
FieldTypeRequiredDescription
summarystringYesHigh-level description of what the plan accomplishes.
stepsstring[]YesOrdered list of plan steps.

On Claude Code, this maps to EnterPlanMode/ExitPlanMode for a first-class plan UI. On other hosts, it renders as a numbered list with an explicit approval prompt.

act.checklist() — tracked task list

Creates a tracked list of tasks the agent should work through.

import { act, type } from '@contentful/skill-kit';

.step('create-tasks', {
  prompt: act.checklist({
    create: [
      { title: 'Fix CI configuration', status: 'pending' },
      { title: 'Update dependencies', status: 'pending' },
      { title: 'Run integration tests', status: 'pending' },
    ],
  }),
  response: type({ acknowledged: 'boolean' }),
  next: 'execute-tasks',
})
FieldTypeRequiredDescription
createArray<{ title: string; status: string }>YesTasks to register, each with a title and initial status.

act.subagent() — spawn an isolated sub-agent

Delegates a focused piece of work to a sub-agent with its own context window. The sub-agent works independently and returns structured output.

import { act, type } from '@contentful/skill-kit';

const ResearchSummary = type({
  findings: {
    cve: 'string',
    severity: "'low' | 'medium' | 'high' | 'critical'",
    affected: 'string',
  }[],
});

.step('research', {
  prompt: act.subagent({
    prompt: 'Research the top 5 CVEs affecting our dependency tree. Return a structured summary.',
    output: ResearchSummary,
  }),
  response: ResearchSummary,
  next: 'incorporate-findings',
})
FieldTypeRequiredDescription
promptstringYesInstructions for the sub-agent.
outputtype.AnyYesArkType schema the sub-agent’s result must match.
allowRecursionbooleanNoDefault false. When false, the rendered XML includes a no-recurse attribute set to the skill name, preventing the subagent from re-invoking the same skill. When true, no guard is emitted.

On hosts without real agent isolation (no Agent tool), the prose fallback still produces correct output but doesn’t get the isolated context-window benefit.

act.survey() — batched questions

Collects answers to multiple questions in a single step, presented as a batch:

import { act, type } from '@contentful/skill-kit';

.step('gather-preferences', {
  prompt: act.survey({
    questions: [
      { key: 'language', question: 'Preferred programming language?' },
      { key: 'editor', question: 'Preferred editor or IDE?' },
      { key: 'os', question: 'Primary operating system?' },
    ],
  }),
  response: type({
    language: 'string',
    editor: 'string',
    os: 'string',
  }),
  next: 'done',
})

XML output format and host mapping

The SDK renders all prompt segments as XML tags. The preamble (sent once at session start) maps each tag to the host’s tool via a markdown table:

TagDescriptionExample tool (Claude Code)
<system>Behavioral directives (persona, tone)---
<prompt>Task instructions (plain strings get wrapped)---
<ask-user>Structured or open questionAskUserQuestion
<confirm>Binary yes/no confirmationAskUserQuestion
<plan>Plan presentation with stepsEnterPlanMode
<checklist>Tracked task listTaskCreate
<subagent>Sub-agent delegation (no-recurse guard)Agent

No tool names appear in the XML itself. The preamble table maps tags to tools. On hosts without a matching tool, the instruction column provides generic fallback behavior (e.g., present a numbered list for <ask-user>).

Same skill, same XML, every host. The preamble handles the translation.

Fragments and prompts

Two utilities for composing prose across steps and skills.

fragment() — named prose snippets

Fragments are reusable chunks of prose with a name for tracking and deduplication.

import { fragment } from '@contentful/skill-kit';

export const enterpriseTone = fragment(
  'enterprise-tone',
  `Use a professional, concise tone. Avoid jargon unless the user
   introduced it. Prefer concrete recommendations over hedging.`,
);

export const jsonOutputRules = fragment(
  'json-output',
  `Return only valid JSON matching the schema. No preamble,
   no markdown fences, no commentary outside the object.`,
);
ArgumentTypeDescription
namestringRequired. Identifier for tooling, tracking, and deduplication.
contentstringThe prose text. Automatically trimmed.

The returned Fragment object is frozen with { name, content }.

prompt — tagged template literal with auto-dedent

The prompt tag handles indentation cleanup and fragment interpolation. Without it, template literals in indented TypeScript produce ugly whitespace.

import { prompt } from '@contentful/skill-kit';
import { enterpriseTone, jsonOutputRules } from '../fragments/tone.js';

.step('analyze', {
  prompt: ({ store }) => prompt`
    ${enterpriseTone}

    Analyze the dependency tree at ${store.steps.gather.repoPath}.
    Flag packages with known CVEs.

    ${jsonOutputRules}
  `,
  response: AnalysisSchema,
  next: 'report',
})

How it works:

This is the only sugar the SDK provides for prose authoring. Everything else is standard functions.