Skip to main content
Nika is a small language ( verbs, 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 workflows are written. Each links the canonical example that embodies it.
The spec’s normative cousin of this page is the one-obvious-way table (linter rules one-obvious-way/001-007). This page is the engineering judgment around those rules.

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.
# ✅ 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 (zero model calls) · Config drift sentinel · 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 · Social repurpose (the diamond) · 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 · Contract guard (schema + validate + assert, belt-and-braces) · 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 · Localization factory · 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.

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 (human gate) · Incident war room (assert refuses optimistic postmortems) · 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 canonical providers make this a one-line decision, not an architecture meeting. Taught by · Contract guard · 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 (the read-only swarm) · Code review (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 · 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 (the transpose zip) · 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 ({brief, sources}) · Schema retry (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 — and the state-file pattern makes even stateful workflows replayable.

The shape of a well-written file

the shape · a skeleton, not a runnable file
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:
- 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:
- 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:
- 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:
- 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.

See every pattern live

The examples gallery — 20 real jobs, every construct taught, every file conformance-validated.