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¶
- Start from the skeleton below (or the closest example).
- Fill the faces state by state; every path must reach
ENDwithinbudget. - Validate:
mklang check my.mk(schema + semantics), thenmklang lint my.mk(dead gates, unread outputs, typos). - Pin behavior without keys:
mklang test my.mk --script my.test.yaml. - Run:
mklang run my.mk --set key=value— or commission it from an MCP host via theruntool (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 underoutputinstead of overwriting (§4.6).sample: N/over: "{{list}}"— fan-out; output becomes a list; mutually exclusive;{{index}}available inside both,{{item}}insideover(§4.7).parse: list— deposit a parsed JSON array instead of text, ready for a downstreamover:; declaremklang: "0.3"(§4.10).call: <machine>— run a sibling machine as a subroutine;input:maps parent context in,output:receives itsresult(§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: otherwisecatch-all last — gates after it are dead, and without it an unmatched judgment halts the run. - A state whose only exits are
repairis a guaranteed halt once the repair budget runs out — always pair it with anok/escalate/failroute. - 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 bytier:(fast/balanced/reasoning) only; the host config maps tiers to models (ADR 0003). - No side effects in prose.
executionis policy, not action; anything that touches the world is atool:state (and listed under top-leveltools:). - System vs user. Sticky role/guardrails →
structure/execution(system channel in the reference interpreter). Turn data and all{{…}}→promptonly (structure/executionare not interpolated). - Every
{{key}}must resolve: fromcontext:, a previous state'soutput:,human.*(HITL resume), oritem/indexinside a fan-out state. call:targets must exist as sibling.mkfiles (same directory), a bundledstd_*name, or an entry-point machine — an inline MCPsourcemachine cancall: std_*but not arbitrary siblings unless the host registry includes them.- Time-sensitive / web machines: declare
today: ""incontext:; the host fills the ISO date when empty. For wall-clock time declarenow: ""(local ISO datetime). Ground web answers intool: searchnotes, 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
$nowkeyword, inline bash, or provider ids). Use declaredcontext.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.