> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nika.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# How to write Nika

> The twelve patterns of well-written Nika — each one taught by a canonical example, each anti-pattern named.

export const SHOWCASE = {
  count: 20,
  t1: 4,
  t2: 7,
  t3: 5,
  t4: 4
};

export const CANON = {
  schemaVersion: 1,
  verbs: 4,
  verbNames: ["infer", "exec", "invoke", "agent"],
  namespaces: 5,
  namespaceNames: ["vars", "with", "tasks", "env", "secrets"],
  builtins: 23,
  builtinNames: ["assert", "compose", "convert", "date", "done", "edit", "emit", "fetch", "glob", "grep", "hash", "inspect", "jq", "json_diff", "json_merge_patch", "log", "notify", "prompt", "read", "uuid", "validate", "wait", "write"],
  providers: 14,
  providersCloud: 8,
  providersLocal: 5,
  providersTest: 1,
  providerIdsCloud: ["mistral", "anthropic", "openai", "gemini", "deepseek", "xai", "groq", "openrouter"],
  providerIdsLocal: ["ollama", "lmstudio", "llamacpp", "localai", "vllm"],
  providerIdsTest: ["mock"],
  extractModes: 9,
  extractModeNames: ["article", "feed", "jq", "links", "markdown", "metadata", "selector", "sitemap", "text"],
  errorNamespaces: 14,
  errorNamespaceNames: ["NIKA-AGENT", "NIKA-BUILTIN", "NIKA-CANCEL", "NIKA-DAG", "NIKA-EXEC", "NIKA-IMPL", "NIKA-INFER", "NIKA-INVOKE", "NIKA-MCP", "NIKA-PARSE", "NIKA-PROVIDER", "NIKA-SEC", "NIKA-TIMEOUT", "NIKA-VAR"],
  errorCategories: 12,
  errorCodes: 50,
  pillars: 5
};

Nika is a small language ({CANON.verbs} verbs, {CANON.namespaces}
namespaces, one expression surface), so good Nika is mostly about
shape: where the determinism lives, where the model is allowed to
think, and how data crosses task boundaries. These twelve patterns are
how the {SHOWCASE.count} showcase workflows are written. Each links
the canonical example that embodies it.

<Tip>
  The spec's normative cousin of this page is the
  [one-obvious-way table](https://github.com/supernovae-st/nika-spec/blob/main/spec/03-dag.md)
  (linter rules `one-obvious-way/001-007`). This page is the
  engineering judgment *around* those rules.
</Tip>

## 1 · Deterministic core, model at the edges

jq decides; the model explains. Anything that can be computed
(filtering, sums, diffs, ranking) happens in `nika:jq` or a data
builtin, deterministically and for free. The model gets the jobs only
a model can do: judgment, language, synthesis.

```yaml theme={"system"}
# ✅ jq filters · the model only explains what survived
- id: urgent
  invoke:
    tool: "nika:jq"
    args: { input: "${{ tasks.triage.output.tickets }}", expression: 'map(select(.urgency == "critical"))' }
- id: explain
  depends_on: [urgent]
  when: ${{ size(tasks.urgent.output) > 0 }}
  infer: { prompt: "Explain these critical tickets · ${{ tasks.urgent.output }}" }
```

**Anti-pattern** · asking the model to « pick the urgent ones » from a
list it already returned typed. You pay tokens for worse determinism.

Taught by · [Price watch](/examples/price-watch) (zero model calls) ·
[Config drift sentinel](/examples/config-drift-sentinel) ·
[ETL quarantine](/examples/etl-quarantine).

## 2 · Parallelism is the default · `depends_on` is the only ordering

Tasks with no edge between them run together. Don't serialize out of
habit: declare the real data dependencies and let the engine schedule
the waves. If you reference `${{ tasks.X }}` you must declare the
edge; the DAG has no invisible edges (`NIKA-DAG-003`).

**Anti-pattern** · a linear chain of tasks that never read each
other's output. That is wall-clock spent on nothing.

Taught by · [Standup digest](/examples/standup-digest) ·
[Social repurpose](/examples/social-repurpose) (the diamond) ·
[CEO Monday brief](/examples/ceo-monday-brief) (3-branch gather).

## 3 · Type the boundaries

Every place data crosses from a model into the deterministic world gets
a contract: `schema:` on `infer`/`agent` (the model must return that
shape), `nika:validate` for second opinions, `nika:assert` as the hard
gate. Enums kill « kinda-strong » ratings.

**Anti-pattern** · prose in, prose out, regex in the middle. If a
downstream task indexes into a field, the producer needs a schema.

Taught by · [Meeting actions](/examples/meeting-actions) ·
[Contract guard](/examples/contract-guard) (schema + validate + assert,
belt-and-braces) · [Support triage](/examples/support-triage) (enums).

## 4 · Fan out with a leash

`for_each` over a runtime collection is the power move. Bound it with
`max_parallel` (providers rate-limit, GPUs thrash), make it resilient
with `fail_fast: false` (collect errors instead of aborting the batch)
and give each iteration its own `retry` and `timeout`.

**Anti-pattern** · an unbounded fan-out against a rate-limited API, or
leaving `fail_fast` on its default (true) for a batch where one bad
item is normal.

Taught by · [Competitor radar](/examples/competitor-radar) ·
[Localization factory](/examples/localization-factory) ·
[Resume screener](/examples/resume-screener).

## 5 · Plan, then execute

For open-ended work: a fast model writes a typed plan, an `agent:`
executes it under budgets, a thinking model synthesizes. Three stages,
three cost profiles, every intermediate auditable on disk.

**Anti-pattern** · one giant agent loop with no plan, no budget and no
typed output. Nothing about it can be audited or bounded.

Taught by · [Deep research brief](/examples/deep-research-brief).

## 6 · Three gates, three meanings

* `when:` · the **skip** gate. Routing, not failure (skipped ≠ failed).
* `nika:assert` · the **fail-fast** gate. The run is wrong, stop loudly.
* `nika:prompt` · the **human** gate. Blocks until a person decides.

Pick the gate that matches the meaning. A workflow that sends drafts
without a `prompt` gate, or claims success without an `assert`, is
promising more than it checks.

**Anti-pattern** · `when: ${{ tasks.a.status == 'success' }}` as a
plain success gate. `depends_on` already does that (linter
`one-obvious-way/001`).

Taught by · [Invoice chaser](/examples/invoice-chaser) (human gate) ·
[Incident war room](/examples/incident-war-room) (assert refuses
optimistic postmortems) · [Release train](/examples/release-train)
(all three in one file).

## 7 · Sovereignty is a `model:` line

Sensitive data (contracts, CVs, medical, financial) runs on a local
provider, `ollama/…` or `lmstudio/…`. Same file shape, zero cloud. The
{CANON.providers} canonical providers make this a one-line decision,
not an architecture meeting.

Taught by · [Contract guard](/examples/contract-guard) ·
[Resume screener](/examples/resume-screener).

## 8 · Agents get budgets, tools get grants

`agent:` is default-deny: no `tools:` means no tools at all. Grant the
minimum (`nika:read` + `nika:done` makes a read-only reviewer), cap
the loop (`max_turns` · `max_tokens_total`), and let `nika:done` end
it cleanly.

**Anti-pattern** · `tools: ["nika:*", "mcp:*/*"]` on an agent that
only needed to read. Least privilege costs one line less.

Taught by · [PR review fan-out](/examples/pr-review-fanout) (the
read-only swarm) · [Code review](https://github.com/supernovae-st/nika-spec/blob/main/examples/23-code-review.nika.yaml)
(foundation).

## 9 · Evidence always lands · `on_finally`

Two tools, one rule. **`on_finally:`** is per-task cleanup — it fires
when that task *ran* (success, failure, timeout, mid-flight cancel).
**A terminal `when: true` task** is the always-pattern — it runs on
EVERY outcome, including upstreams that failed or never started
(`when: true` replaces the default success-gate). Cleanup belongs to
the task; the record that must land at 3am belongs to a terminal task.

Taught by · [Incident war room](/examples/incident-war-room) ·
[Release train](/examples/release-train) (the departure record is a
`when: true` terminal task — it lands even when the train aborts).

## 10 · One data language · jq, once

`output:` bindings, `nika:jq`, the fan-in zip (`transpose`), the
state diff: all of it is the same jq. Don't invent per-task string
parsing and don't ask the model to reshape JSON. One transform
language is already there.

Taught by · [Localization factory](/examples/localization-factory)
(the transpose zip) · [ETL quarantine](/examples/etl-quarantine)
(group\_by accounting).

## 11 · Workflows are callable · type the `outputs:`

A workflow with typed `outputs:` is a building block: another workflow
(or a human, or CI) consumes a contract, not a log. Name what comes
out, type it, describe it.

Taught by · [Deep research brief](/examples/deep-research-brief)
(`{brief, sources}`) · [Schema retry](https://github.com/supernovae-st/nika-spec/blob/main/examples/19-schema-retry.nika.yaml)
(foundation · the typed-outputs shape).

## 12 · Mock-first · runnable with zero keys

`model: mock/echo` makes a workflow CI-runnable and demo-safe. Write
it mock-first and swap the provider when it ships. Every example in
this documentation that can run without keys does.

**Anti-pattern** · a workflow you can't validate without spending real
tokens. The conformance gate runs every example on every push, and
yours should pass the same way.

Taught by · the whole [examples pack](/examples/overview) — and the
[state-file pattern](/examples/release-radar) makes even stateful
workflows replayable.

***

## The shape of a well-written file

```yaml the shape · a skeleton, not a runnable file theme={"system"}
nika: v1                      # the contract · one line · forever
workflow: kebab-case-name     # resource name
description: "…"              # one honest sentence

model: mock/echo              # mock-first · sovereignty is one line away

vars:                         # typed where it matters · required: true documents itself
secrets:                      # vault/env-backed · never inline

tasks:                        # the DAG · true dependencies only ·
  # deterministic core (jq · builtins) · model at the edges ·
  # gates that match their meaning · leashed fan-outs · budgeted agents

outputs:                      # the callable contract · typed
```

***

## Four recipes the patterns compose into

The 12 patterns are the values; these are the moves you reach for when a
real integration pushes back. Each is canonical in the spec — linked, not
improvised.

### Poll until ready

`retry:` fires on errors, never on values — so make « not ready » an error
*inside* the task. A jq-mode fetch whose program errors on the pending shape
turns polling into typed, bounded retry:

```yaml theme={"system"}
- id: await_export
  invoke:
    tool: "nika:fetch"
    args:
      url: "https://api.example.com/jobs/${{ vars.job_id }}"
      mode: jq
      jq: 'if .status == "done" then . else error("not ready") end'
  retry:
    max_attempts: 20
    backoff_strategy: fixed
    backoff_ms: 30000          # every 30s · 20 attempts · 10-minute ceiling
```

Bounded, deterministic, zero LLM. (`retry_when:` — retrying on a value
condition directly — is reserved for a future minor.)

### Diamond join

Two exclusive `when:` branches, one consumer. A skipped branch's output is
**defined `null`** (never an error), so the join is one jq filter:

```yaml theme={"system"}
- id: pick
  depends_on: [build_prod, build_dev]
  invoke:
    tool: nika:jq
    args:
      input: [ "${{ tasks.build_prod.output }}", "${{ tasks.build_dev.output }}" ]
      expression: "[ .[] | select(. != null) ] | first"
```

### Fan-out that survives partial failure

A failed iteration contributes `null` at its index — positions stay aligned
with the input. Recover per-iteration when a placeholder is acceptable,
filter downstream:

```yaml theme={"system"}
- id: scrape_all
  for_each: ${{ tasks.discover.pages }}
  max_parallel: 5
  fail_fast: false
  on_error: { recover: null }        # this iteration yields null · the batch lives
  invoke:
    tool: "nika:fetch"
    args: { url: "${{ item }}", mode: article }

- id: digest
  depends_on: [scrape_all]
  invoke:
    tool: nika:jq
    args:
      input: ${{ tasks.scrape_all.output }}
      expression: "[ .[] | select(. != null) ]"   # the survivors · order preserved
```

### Matrix expansion

A matrix is precomputed data, not control flow. Build the product with jq,
fan out over it:

```yaml theme={"system"}
- id: build_matrix
  invoke:
    tool: nika:jq
    args:
      input: { os: ["linux", "macos"], ver: ["18", "20", "22"] }
      expression: "[ .os[] as $o | .ver[] as $v | {os: $o, ver: $v} ]"

- id: test_all
  depends_on: [build_matrix]
  for_each: ${{ tasks.build_matrix.output }}
  max_parallel: 3
  exec:
    command: "./test.sh --os ${{ item.os }} --node ${{ item.ver }}"
```

Include/exclude rules are jq filters on the product — no second syntax to
learn.

<Card title="See every pattern live" icon="rocket" href="/examples/overview">
  The examples gallery — 20 real jobs, every construct taught, every
  file conformance-validated.
</Card>
