Skip to content

Authoring guide

A compact recipe for writing a correct .mk — aimed at LLM agents and humans who want the happy path without reading the full SPEC first. This page distills; it does not replace. Operating guidance lives in Patterns; the full do / don't checklist is Best practices.

The recipe

  1. Start from the skeleton below (or the closest example).
  2. Fill the faces state by state; every path must reach END within budget.
  3. Validate: mklang check my.mk (schema + semantics), then mklang lint my.mk (dead gates, unread outputs, typos).
  4. Pin behavior without keys: mklang test my.mk --script my.test.yaml.
  5. Run: mklang run my.mk --set key=value — or commission it from an MCP host via the run tool (mklang-mcp, inline source or path).

Skeleton

Start every file with the schema header — editors and agents get validation for free:

# yaml-language-server: $schema=../schema/mklang.schema.json
mklang: "0.3" # language version (0.2 documents remain valid)
machine: my_machine # identifier (snake_case)
entry: first_state # where the run starts
budget: 6 # max steps per run — shortest path + headroom (see below)
result: answer # context key returned to the caller (default: last output)
context: # initial blackboard (optional); --set / inputs merge here
  question: ""
states:
  first_state:
    structure: a short answer, max 50 words # what shape?
    prompt: "Answer: {{question}}" # what to think?
    output: answer # stored under this context key
    gates:
      - when: the answer addresses the question
        then: ok
        to: END
      - when: otherwise # ALWAYS end with a catch-all
        repair: 2
        to: first_state

Required top-level keys: machine, entry, budget, states. A state is exactly one of generative (prompt), call (call), or tool (tool).

The faces of a state

Core (generative states — SPEC §4):

Face Answers LLM channel (ref. interpreter) Notes
structure what shape? system (produce) output contract; not interpolated
execution how to act? system (produce) sticky policy — never side effects; not interpolated
prompt what to think? user (produce) task + data; {{context.key}} interpolation
gates when to exit? judge (separate call) transition table (below)

Durable role/constraints → structure / execution. Turn data ({{today}}, history, user text) → prompt only. Details: Best practices §3.

Optional faces — reach for them when the pattern calls for it:

  • reason: true — private chain-of-thought, captured in the trace (§4.5).
  • accumulate: true — append to a list under output instead of overwriting (§4.6).
  • sample: N / over: "{{list}}" — fan-out; output becomes a list; mutually exclusive; {{index}} available inside both, {{item}} inside over (§4.7).
  • parse: list — deposit a parsed JSON array instead of text, ready for a downstream over:; declare mklang: "0.3" (§4.10).
  • call: <machine> — run a sibling machine as a subroutine; input: maps parent context in, output: receives its result (§4.8).
  • tool: <name> — a host-registered callable; the only place for real side effects (search, send, calc) (§4.9).

Gates are the transitions

Each gate is a prose condition the LLM judges (or a deterministic hook:), plus exactly one policy (SPEC §5):

Policy Effect
then: ok advance to to: (END finishes the run)
repair: N re-run this state with feedback, at most N times
escalate: true route to a handler state (suspends under HITL)
fail: true abort the run

Rules of thumb:

  • Multi-gate states need a when: otherwise catch-all last — gates after it are dead, and without it an unmatched judgment halts the run.
  • A state whose only exits are repair is a guaranteed halt once the repair budget runs out — always pair it with an ok/escalate/fail route.
  • Exact checks (thresholds, format, policy) belong in a hook: gate, not prose.

Budget

budget counts steps (states entered). Set it to the shortest entry → END path plus headroom for loops and repairs: check rejects budget-infeasible (below the shortest path) and warns when there is no headroom. A fan-out state charges one step per branch at runtime but counts as 1 at check time.

Hard rules

  • Never name a provider or model in a .mk — route by tier: (fast / balanced / reasoning) only; the host config maps tiers to models (ADR 0003).
  • No side effects in prose. execution is policy, not action; anything that touches the world is a tool: state (and listed under top-level tools:).
  • System vs user. Sticky role/guardrails → structure / execution (system channel in the reference interpreter). Turn data and all {{…}}prompt only (structure/execution are not interpolated).
  • Every {{key}} must resolve: from context:, a previous state's output:, human.* (HITL resume), or item/index inside a fan-out state.
  • call: targets must exist as sibling .mk files (same directory), a bundled std_* name, or an entry-point machine — an inline MCP source machine can call: std_* but not arbitrary siblings unless the host registry includes them.
  • Time-sensitive / web machines: declare today: "" in context:; the host fills the ISO date when empty. For wall-clock time declare now: "" (local ISO datetime). Ground web answers in tool: search notes, not training knowledge (Best practices §6).
  • Exact policy (thresholds, allowlists) → hook: gates, not prose alone.
  • Do not invent language syntax outside the schema (no ad-hoc $now keyword, inline bash, or provider ids). Use declared context.now + host fill, or host tools/hooks entry points.

What the validators catch

mklang check errors (blocking) and warnings you will actually see:

entry 'x' is not a state
draft: gate -> unknown state 'sendd'            # typo in to:
combine: call -> unknown machine 'summarize'    # missing sibling .mk
no reachable path to END
budget-infeasible: budget 2 is below the 4-step shortest path to END
draft: no 'otherwise' catch-all gate            # warning
result key 'answr' is not produced by any state's output   # warning

mklang lint adds static smells: dead gates after otherwise, repair-only states, outputs never read, and unresolved-interpolation (a {{key}} whose root is no context key or output — typos like {{ticket.bod}} included). --strict promotes lint findings to failures.

Which example to copy

Before writing a generic architecture yourself, check the machine stdlib: CoT, self-consistency, refine, ToT, debate, map-reduce and cascade ship as ready std_* machines you can call: or run by name.

Pattern Example
Minimal single state summarize_doc.mk
Branching FSM + real tools + scenario test triage.mk
Reason/act/observe loop (accumulate) react.mk
Iterative loop (training knowledge only) research.mk
Research + host tool: search research_web.mk
Research + explicit notes compression research_compress.mk
News brief + today + search recency news_search.mk (+ .test.yaml)
Fan-out sample + reducer self_consistency.mk
over + call orchestration map_reduce.mk
Deterministic hook gates hook_gates.mk
Divergent terminals + fail expense_approval.mk

Pre-flight (copy this)

mklang check my.mk && mklang lint --strict my.mk
mklang test my.mk --script my.test.yaml    # if scenarios exist
# live only after scenarios are green:
mklang run my.mk --set key=value

Go deeper

  • Best practices — checklist, tool contracts, anti-patterns, language vs host boundaries.
  • Patterns — tier routing, reliability tuning, mklang test, which architecture when.
  • SPEC — the full semantics; §10 is the pattern cookbook.
  • Schema — structural validation for editors.