Matrix logo

Writing a SKILL.mtx

A skill is the unit of compile-time knowledge in MCL. It tells the compiler what kind of NL requests it handles, what it needs to know, how to extract a typed Frame from them, a...

A skill is the unit of compile-time knowledge in MCL. It tells the compiler what kind of NL requests it handles, what it needs to know, how to extract a typed Frame from them, and what can go wrong.

Each skill lives at skills/<slug>/SKILL.mtx. The compiler selects the skill that best matches the verb and the user's intent, then runs the skill's §PROCEDURE to produce the Frame.


Required sections

Every SKILL.mtx must have exactly these 8 sections (validator rule V1):

§SKILL
§INPUTS
§CORTEX
§TOOLS
§SUB_SKILLS
§PROCEDURE
§OUTPUTS
§FAILURE_MODES

§HASH is optional and added by tooling. Don't write it by hand.


A minimal working example

§SKILL
name="Writing Plans"
version=1.0.0
mcl.verbs=build modify
description="Creates or updates a structured plan document"

§INPUTS
slot target: ArtifactRef
  required
  hint="The plan document to create or update"

slot goal: string
  required
  hint="What the plan should achieve"

slot deadline: iso8601
  optional
  hint="Target completion date"

§CORTEX
reads=Goal Preference Pattern Fact

§TOOLS
matrix://tool/mcp/filesystem/fs_write@0.1.0
matrix://tool/mcp/filesystem/fs_read@0.1.0

§SUB_SKILLS
none

§PROCEDURE

on verb=build
  kind="write"
  prompt
    system="You are an expert technical planner. Extract a structured plan from the user's request.\n\nYou have access to the user's context:\n{cortex.bundle}\n\nExtract these fields as JSON:\n- goal: string (the high-level objective)\n- milestones: string[] (ordered deliverables)\n- constraints: object[] (budget/deadline/scope constraints)\n- success_criteria: object[] (measurable completion criteria)"
    user="Goal: {prose}\n\nTarget: {slot.target}\nDeadline: {slot.deadline}"
  end
  resolve slot.target <- cortex.find(type="ArtifactRef", near=slot.target.prose)
end

on verb=modify
  kind="write"
  prompt
    system="You are an expert technical planner. The user wants to update an existing plan.\n\nContext:\n{cortex.bundle}\n\nExtract the requested changes as JSON."
    user="Modification request: {prose}\n\nExisting plan: {slot.target}"
  end
  resolve slot.target <- cortex.find(type="ArtifactRef", near=slot.target.prose)
end

on unknown
  unknown slot.target
    severity=blocking
    reason="Cannot proceed without knowing which plan to work on"
  end
end

§OUTPUTS
slot result: ArtifactRef
  hint="The created or updated plan document"

§FAILURE_MODES

target_not_found
  action=gate
  reason=unknown_information
  suggest=amend_constraint

scope_too_broad
  action=retry
  reason=ambiguous_request
  suggest=amend_constraint

§SKILL metadata

§SKILL
name="Human-readable skill name"    # must be double-quoted
version=1.0.0                        # semver
mcl.verbs=build modify deliver       # D7 closed set; space-separated list
description="What this skill does"  # must be double-quoted

mcl.verbs controls which verb classifications this skill handles. The compiler's skill router uses this to narrow the candidate set. An intent classified as verb=find will never dispatch to a skill that only declares build modify.


§INPUTS

Declares the typed slots the compiler needs to fill:

§INPUTS
slot target: ArtifactRef
  required
  hint="The document or system to operate on"

slot amount: AssetAmount
  optional
  default=100.00

slot format: enum<pdf|markdown|html>
  optional
  default=markdown
  hint="Output format"

Always double-quote hint= values. Unquoted, the lexer treats the words as a space-separated ident list and the value is wrong.

Slots declared here are what resolve and unknown blocks can reference. Referencing a slot name not declared here is a V5/V6 validation error.


§CORTEX

Declares which cortex memory types this skill reads for its context bundle:

§CORTEX
reads=Goal Preference Constraint Fact Pattern

This is a space-separated list of cortex memory type names. The stage 3 cortex pre-fetch uses this (combined with the verb routing table in core/verb.mtx) to build the context bundle fed into the skill's prompt.


§TOOLS

Lists the version-pinned tools the skill's plan steps may call. Must be version-pinned (V10):

§TOOLS
matrix://tool/mcp/filesystem/fs_write@0.1.0
matrix://tool/mcp/filesystem/fs_read@0.1.0
matrix://tool/mcp/tachyon/tachyon_compile@0.1.0

@latest is rejected. If a tool is undeclared here but a plan step tries to call it, the executor's allowlist check fails.

Use none if the skill doesn't call any tools:

§TOOLS
none

§SUB_SKILLS

Lists sub-skills this skill may dispatch to. Same rules as §TOOLS:

§SUB_SKILLS
matrix://skill/code-review@1.0.0
matrix://skill/writing-plans@2.1.0

Use none if there are no sub-skills.


§PROCEDURE

This is the main event. One or more on-blocks that define what the compiler does for each verb or condition.

Top-to-bottom, first-match-wins

The interpreter walks on-blocks from top to bottom and executes the first one whose condition matches. Order matters.

Standard pattern: verb-branch per verb + unknown fallback

§PROCEDURE

on verb=build
  prompt
    system="..."
    user="..."
  end
  resolve slot.target <- cortex.find(type="ArtifactRef", near=slot.target.prose)
end

on verb=modify
  prompt
    system="..."
    user="..."
  end
  resolve slot.target <- cortex.find(type="ArtifactRef", near=slot.target.prose)
end

on unknown
  unknown slot.target
    severity=blocking
    reason="Tell me what you want to work on."
  end
end

Prompts

The prompt block is what drives stage 4. It gets interpolated and sent to the LLM with the grammar constraint.

on verb=build
  kind="write"
  prompt
    system="You are a plan writer. Extract a structured plan.\n\nContext: {cortex.bundle}"
    user="User goal: {prose}\n\nDeadline: {slot.deadline}"
  end
end

Keep prompts focused and concrete. The grammar constraint (intent_frame@1) already shapes the output — the prompt's job is to focus the model on the right fields. Over-prompting tends to hurt more than help.

Resolving entity references (D13)

Every NL entity reference in the user's input must be resolved to a matrix:// URI before the user signs. This happens via resolve statements:

resolve slot.target <- cortex.find(type="ArtifactRef", near=slot.target.prose)

cortex.find is the most common: it does a typed predicate lookup in the actor's cortex. The near= argument specifies the NL text to match semantically.

cortex.resolve is for exact resolution when you know the entity name:

resolve slot.project <- cortex.resolve(slot.project.prose)

cortex.context is for fetching a full context bundle into a slot (useful when a slot holds context rather than a specific entity):

resolve slot.user_context <- cortex.context(verb=slot.verb)

Declaring gaps

When a slot is unavailable and resolution fails, declare it as a gap:

on unknown
  unknown slot.target
    severity=blocking
    reason="I need to know which document to work on."
    options=[README CHANGELOG spec ARCHITECTURE]
  end
  unknown slot.deadline
    severity=optional
    reason="A deadline would help prioritize the plan."
    default="next sprint"
  end
end

blocking severity stops execution. preferred and optional are advisory — the intent can proceed without them, but a clarify question is still generated.

Confidence branching

Handle low-confidence cases explicitly:

on confidence<0.75
  prompt
    system="The user's intent is ambiguous. Ask exactly one clarifying question."
    user="Original: {prose}\n\nWhat is unclear?"
  end
  clarify slot.target
    prompt="Which document or system are you referring to?"
    type=ArtifactRef
    required=true
  end
end

Slot value branching

Branch on a specific slot value:

on slot.format=pdf
  prompt
    system="Generate a PDF-formatted plan structure."
    user="{prose}"
  end
end

Step kind annotations

Add kind= inside the on-block to route the executor's plan step to the right model tier:

on verb=build
  kind="write"     # prose specialist model
  ...
end

on verb=analyze
  kind="reason"    # default reasoning model
  ...
end

This is metadata for the executor — it doesn't affect compile-time behaviour. Leave it out if you're not sure; "reason" is the default.

Output cardinality hints

When the skill naturally produces N independent outputs (e.g. "write 5 blog post titles"), annotate it:

on verb=build
  output_cardinality=5
  ...
end

The planner folds this into a single multi-output step or a parallel{} fan-out. Must be a strictly positive integer. Validator rule V12 rejects zero, negatives, or non-integer values.


§OUTPUTS

Declares what the skill produces:

§OUTPUTS
slot result: ArtifactRef
  hint="The created document"

slot summary: string
  hint="A one-paragraph summary of what was done"

Output slots are what plan steps write to. The executor validates that the declared expected outputs were produced.


§FAILURE_MODES

Named failure scenarios with action and reason:

§FAILURE_MODES

cannot_locate_target
  action=gate
  reason=unknown_information
  suggest=amend_constraint

budget_exceeded
  action=fail
  reason=out_of_budget
  suggest=raise_budget

ambiguous_scope
  action=retry
  reason=ambiguous_request
  suggest=amend_constraint

The reason= value must be in the closed set (V8): unknown_information, policy_violation, out_of_budget, out_of_scope, ambiguous_request, tool_failure, external_failure, timeout, cancelled_by_user, correction_invalid.


Validation

# Validate a SKILL.mtx
mclc validate skills/my-skill/SKILL.mtx

# Compute the canonical hash
mclc hash skills/my-skill/SKILL.mtx

# Check what the compiled prompt would look like without calling the LLM
mclc compile \
  -skill skills/my-skill/SKILL.mtx \
  -prose "Build a deployment plan for my Node.js app" \
  -verb build \
  -dry-run

Validation is strict about a few things that are easy to get wrong:

  1. String values for hint=, reason=, prompt=, description= must be double-quoted. The lexer invariant means unquoted text parses as a space-separated ident list, not a string. The validator doesn't always catch this directly, but the interpreter will silently get the wrong value.

  2. §TOOLS and §SUB_SKILLS URIs must be version-pinned. @latest is rejected by V9/V10. Pin to @semver or @sha256:....

  3. Prompt blocks inside on-blocks must have both system= and user=. Missing either triggers V7.

  4. Slot names in resolve and unknown must be declared in §INPUTS. V5 and V6.

  5. kind= values must be in the closed set. V11.


Testing a skill

The simplest way to test whether a skill parses and validates:

mclc validate skills/my-skill/SKILL.mtx

To see what prompts get generated without an API key:

mclc compile -skill skills/my-skill/SKILL.mtx \
  -prose "your test input here" \
  -verb build \
  -dry-run

Output is a JSON object with prompt_messages, slots, unknowns, and clarify_questions. The prompt_messages show the exact interpolated text that would be sent to the LLM.

With an API key set:

FIREWORKS_API_KEY=your_key mclc compile \
  -skill skills/my-skill/SKILL.mtx \
  -prose "Build a deployment pipeline for my Node.js app" \
  -verb build

This runs the full pipeline and outputs the frame_json the LLM produced.