Authoring Nika workflows
Nika turns repeatable AI work into files: one .nika.yaml, four verbs,
audited before it runs. You author the file; nika check is the
oracle; the human runs it.
The loop (always)
- Start from a template or example, never from scratch:
nika examples list · nika examples show <slug> ·
nika new --from <template> <file>.nika.yaml
- Write the file. Envelope is always
nika: v1 +
workflow: <kebab-id> + tasks:. Pick models and builtins from
the embedded catalogs — nika catalog (providers · models ·
capabilities · which env var each needs) and nika tools (the
nika:* builtins an invoke reaches without MCP); before a run,
nika inspect <file> shows the anatomy: tasks · waves · the cost
floor.
- Check it:
nika check <file> (exit 0 = clean · 2 = findings),
then nika check --native-strict <file> — it fails on any
native-first hint (an exec: a builtin covers).
- Repair from the diagnostics — they name the exact task, reference
and fix. Unknown code?
nika explain NIKA-XXXX.
- Repeat 3–4 until clean. Never hand a file to the human that does
not pass
nika check — and pass --native-strict too, unless
every remaining exec: is in the exec ledger (below).
- The human (or CI) runs it:
nika run <file>. Preview offline with
--model mock/echo; run locally with --model ollama/<model>.
Inputs ride --var key=value (repeatable · unknown keys refused);
a run paused on a nika:prompt resumes with
nika run <file> --resume <trace> --answer <task>=<value>
(confirm gates take booleans: --answer approve=true).
- Pin it for CI:
nika test <file> --update writes
<file>.golden.json from an offline mock run; nika test <file>
replays and compares — deterministic, zero keys.
- Prove a run that mattered: every run writes a hash-chained
journal to
.nika/traces/ — nika trace verify <trace> checks the
chain (tamper-evidence), nika trace show <trace> reads the card.
Cite the trace, never a memory of the run.
Cost honesty (never hide unknown spend)
nika check prints the cost ceiling BEFORE any token: ≤ $X is a
ceiling · ≥ $X FLOOR means at least one task is unbounded — name
the reason (a missing max_tokens, an uncataloged model, an
expression fan-out), never round it to $0.
- A local model (
ollama/…) is unpriced compute, not « free » —
say "unpriced", never "$0" or "free".
- A spend cap rides the run:
nika run <file> --max-cost-usd <n>
blocks BEFORE the call that would cross the cap.
nika explain <file> narrates all of this (waves · cost · touches ·
how to run) — use it before handing a workflow to a human.
The four verbs (exactly one per task)
infer: — an LLM call (prompt, schema? for typed output,
max_tokens?)
exec: — a shell command (command, capture: text|structured) ·
last resort: run the native-first interrogation first (below)
invoke: — a builtin or MCP tool (tool, args) · HTTP fetch is
tool: "nika:fetch", a tool, not a verb
agent: — a bounded multi-turn loop (prompt, tools allowlist,
max_turns, max_tokens_total)
Native-first (the law)
The order is invoke: nika:* → invoke: mcp:<server>/<tool> →
exec:. Before writing ANY exec:, answer in your head:
- Which builtin replaces it?
nika tools --json is the catalog.
HTTP (curl/wget/helper fetch) → nika:fetch · uploads →
multipart: · site crawls → traverse: · file plumbing
(cat/tee/cp/mkdir) → nika:read/nika:write (create_dirs: true) ·
JSON shaping (jq/sed) → nika:jq (or an output: binding) ·
in-place edits → nika:edit · image/speech provider calls →
nika:image_generate/nika:tts_generate · image styling
(ImageMagick convert / PIL filters / dither scripts) →
nika:image_fx (deterministic — same input+args = same bytes,
the artifact sha256 joins the trace chain).
- Which MCP tool replaces it? A product API deserves an MCP
server, never a helper script.
- Neither? Name the exact gap — then
exec: is legitimate
(build tools · git · a product CLI with no MCP surface yet) and
goes in the ledger.
Never write a helper script (node bin/helper.mjs …) that wraps
HTTP/files/JSON — that is native-first/005, the exact failure class
this law exists for.
Exec ledger (mandatory when any exec remains)
Every surviving exec: gets a row in the workflow's header comment:
# EXEC LEDGER ·
# | task | command | why no native path | unlock that removes it |
--native-strict + a complete ledger = a reviewable workflow.
Discipline
- References:
${{ tasks.<id>.output }} · ${{ vars.x }} ·
${{ env.KEY }} · ${{ secrets.X }} (never inline a credential).
- A task that reads another task's output MUST declare it in
depends_on: [<id>].
- Models are
provider/name (ollama/llama3.2:3b local-first ·
mock/echo offline preview).
- Timeouts are quoted Go-durations (
timeout: "7m") — give local
providers ≥300s: thinking models routinely think past 30s.
- Declare the blast radius:
nika check --infer-permits <file> prints
the tightest permits: block — paste it in (default-deny from then on).
- Structured output: give
infer: a schema:; add
additionalProperties: false for a deterministic shape.
- Auth rides
headers: { x-api-key: "${{ secrets.KEY }}" } (masked ·
declared in secrets: with its egress: sink) — never exec: curl
for the sake of a header.
1---2name: nika3description: Runs repeatable AI work as checked, budgeted workflow files. The agent captures a repeated task as a .nika.yaml DAG, audits cost/permits/schema before a single token is spent, and runs it with tamper-evident traces.4---5
6# Authoring Nika workflows
7
8Nika turns repeatable AI work into files: one `.nika.yaml`, four verbs,
9audited **before** it runs. You author the file; `nika check` is the
10oracle; the human runs it.
11
12## The loop (always)
13
141. **Start from a template or example**, never from scratch:
15 `nika examples list` · `nika examples show <slug>` ·
16 `nika new --from <template> <file>.nika.yaml`
172. **Write the file.** Envelope is always `nika: v1` +
18 `workflow: <kebab-id>` + `tasks:`. Pick models and builtins from
19 the embedded catalogs — `nika catalog` (providers · models ·
20 capabilities · which env var each needs) and `nika tools` (the
21 `nika:*` builtins an `invoke` reaches without MCP); before a run,
22 `nika inspect <file>` shows the anatomy: tasks · waves · the cost
23 floor.
243. **Check it**: `nika check <file>` (exit 0 = clean · 2 = findings),
25 then `nika check --native-strict <file>` — it fails on any
26 `native-first` hint (an `exec:` a builtin covers).
274. **Repair from the diagnostics** — they name the exact task, reference
28 and fix. Unknown code? `nika explain NIKA-XXXX`.
295. Repeat 3–4 until clean. **Never hand a file to the human that does
30 not pass `nika check`** — and pass `--native-strict` too, unless
31 every remaining `exec:` is in the exec ledger (below).
326. The human (or CI) runs it: `nika run <file>`. Preview offline with
33 `--model mock/echo`; run locally with `--model ollama/<model>`.
34 Inputs ride `--var key=value` (repeatable · unknown keys refused);
35 a run paused on a `nika:prompt` resumes with
36 `nika run <file> --resume <trace> --answer <task>=<value>`
37 (confirm gates take booleans: `--answer approve=true`).
387. Pin it for CI: `nika test <file> --update` writes
39 `<file>.golden.json` from an offline mock run; `nika test <file>`
40 replays and compares — deterministic, zero keys.
418. **Prove a run that mattered**: every run writes a hash-chained
42 journal to `.nika/traces/` — `nika trace verify <trace>` checks the
43 chain (tamper-evidence), `nika trace show <trace>` reads the card.
44 Cite the trace, never a memory of the run.
45
46## Cost honesty (never hide unknown spend)
47
48- `nika check` prints the cost ceiling BEFORE any token: `≤ $X` is a
49 ceiling · `≥ $X FLOOR` means at least one task is unbounded — name
50 the reason (a missing `max_tokens`, an uncataloged model, an
51 expression fan-out), never round it to $0.
52- A local model (`ollama/…`) is **unpriced compute, not « free »** —
53 say "unpriced", never "$0" or "free".
54- A spend cap rides the run: `nika run <file> --max-cost-usd <n>`
55 blocks BEFORE the call that would cross the cap.
56- `nika explain <file>` narrates all of this (waves · cost · touches ·
57 how to run) — use it before handing a workflow to a human.
58
59## The four verbs (exactly one per task)
60
61- `infer:` — an LLM call (`prompt`, `schema?` for typed output,
62 `max_tokens?`)
63- `exec:` — a shell command (`command`, `capture: text|structured`) ·
64 **last resort**: run the native-first interrogation first (below)
65- `invoke:` — a builtin or MCP tool (`tool`, `args`) · HTTP fetch is
66 `tool: "nika:fetch"`, a tool, not a verb
67- `agent:` — a bounded multi-turn loop (`prompt`, `tools` allowlist,
68 `max_turns`, `max_tokens_total`)
69
70## Native-first (the law)
71
72The order is `invoke: nika:*` → `invoke: mcp:<server>/<tool>` →
73`exec:`. Before writing ANY `exec:`, answer in your head:
74
751. **Which builtin replaces it?** `nika tools --json` is the catalog.
76 HTTP (curl/wget/helper fetch) → `nika:fetch` · uploads →
77 `multipart:` · site crawls → `traverse:` · file plumbing
78 (cat/tee/cp/mkdir) → `nika:read`/`nika:write` (`create_dirs: true`) ·
79 JSON shaping (jq/sed) → `nika:jq` (or an `output:` binding) ·
80 in-place edits → `nika:edit` · image/speech provider calls →
81 `nika:image_generate`/`nika:tts_generate` · image styling
82 (ImageMagick convert / PIL filters / dither scripts) →
83 `nika:image_fx` (deterministic — same input+args = same bytes,
84 the artifact sha256 joins the trace chain).
852. **Which MCP tool replaces it?** A product API deserves an MCP
86 server, never a helper script.
873. **Neither?** Name the exact gap — then `exec:` is legitimate
88 (build tools · git · a product CLI with no MCP surface yet) and
89 goes in the ledger.
90
91Never write a helper script (`node bin/helper.mjs …`) that wraps
92HTTP/files/JSON — that is `native-first/005`, the exact failure class
93this law exists for.
94
95## Exec ledger (mandatory when any exec remains)
96
97Every surviving `exec:` gets a row in the workflow's header comment:
98
99```
100# EXEC LEDGER ·
101# | task | command | why no native path | unlock that removes it |
102```
103
104`--native-strict` + a complete ledger = a reviewable workflow.
105
106## Discipline
107
108- References: `${{ tasks.<id>.output }}` · `${{ vars.x }}` ·
109 `${{ env.KEY }}` · `${{ secrets.X }}` (never inline a credential).
110- A task that reads another task's output MUST declare it in
111 `depends_on: [<id>]`.
112- Models are `provider/name` (`ollama/llama3.2:3b` local-first ·
113 `mock/echo` offline preview).
114- Timeouts are quoted Go-durations (`timeout: "7m"`) — give local
115 providers ≥300s: thinking models routinely think past 30s.
116- Declare the blast radius: `nika check --infer-permits <file>` prints
117 the tightest `permits:` block — paste it in (default-deny from then on).
118- Structured output: give `infer:` a `schema:`; add
119 `additionalProperties: false` for a deterministic shape.
120- Auth rides `headers: { x-api-key: "${{ secrets.KEY }}" }` (masked ·
121 declared in `secrets:` with its `egress:` sink) — never `exec: curl`
122 for the sake of a header.