AGPL-3.0-or-later · forever.

the skeleton register

etl-state

Your intent sounds like: only what changed since last run · survive bad input One of 10 skeletons the pack ships, green as-is under nika check. Fill the # SLOT: lines, repair from the fix lines, re-check. Machines read the catalog; the binary lists them with nika new '?'.

  • 9SLOT pointsthe only lines you edit
  • 2patterns lockedarrive correct, stay correct
  • 6tools grantedthe minimum for the job
  • 47f2985asha-pinnedre-hashed on every test run

the plan

derived from the file, never drawn by hand
approveinvokeprevious_rawinvoke
previousinvokefreshinvoke
deltainvokesave_stateinvoke
processinvoke

the file, whole

conformance-gated upstream on every spec push
etl-state.nika.yaml
# SPDX-License-Identifier: Apache-2.0# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json## TEMPLATE · etl-state · incremental data job with a state file.##   [approve]──▶[fresh]──┬───────────────────────▶[save_state]   when approved#    prompt      fetch   │                         nika:write#   [previous_raw]─▶[previous]──┴──▶[delta]──when ≠∅──▶[process]#    nika:read      nika:jq          json_diff          nika:jq## The "only what changed since last time" job. A cursor file on disk remembers# the last run, a fetch brings the current truth, and an RFC-6902 diff says# exactly what moved — so downstream work is proportional to the CHANGE, not to# the size of the dataset.## Demonstrates ·#   - a state file read→diff→write cycle · one path, both directions#   - `nika:read` returns TEXT · it is parsed with `fromjson` before diffing#     (skip that and the diff is meaningless — see `previous`)#   - a blocking human gate that DOMINATES every path to the network and the#     write · NEP-0002's Rule of Two, as a check#   - `on_error: on_codes:` · forgive not-found ONLY, never a permission error## TWO HONEST HINTS · this file ships RED-adjacent on purpose:#   [headless-prompt] the gate is deliberately blocking · a `default:` here#     completes the lethal trifecta and lights NIKA-SEC-009 (measured — add one#     and watch the TRIFECTA rung flip). The hint is right that it needs a#     human; that is the point.#   [inputs] `previous_raw` reads a cursor file that does not exist yet · the#     `on_error:` below handles exactly that, but `check` does not model#     recovery. First run creates it.## Needs · nothing to rehearse. For REAL use: a reachable source in#   `const.source_url` — then delete the `on_error:` on `fresh`.## Run · nika run <file> --output json#     The run PAUSES at the gate (exit 4 · durable, not a failure) and prints#     its own resume command. Answer it to continue.nika: v1workflow:  id: etl-state-template        # SLOT: kebab-case workflow id  # SLOT: one honest sentence — `nika new` matches intents against these words  description: "only what changed since last run · read state · fetch fresh · diff · process the delta · save state"const:  source_url: "https://api.example.com/v1/records"   # SLOT: the data source  state_path: "./state/etl-state.json"               # SLOT: the cursor filepermits:                            # the blast radius · default-deny once present  tools: ["nika:fetch", "nika:jq", "nika:json_diff", "nika:prompt", "nika:read", "nika:write"]  net: { http: ["api.example.com"] }    # SLOT: the source host from const.source_url  fs:                               # the cursor file, read then rewritten — one path, both ways    read: ["./state/etl-state.json"]    # SLOT: keep in step with const.state_path    write: ["./state/etl-state.json"]   # SLOT: idem — the job writes nothing elsepolicy:                              # NEP-0014 · the gate runs under a NAMED endorsement mode  endorsement: solotasks:  # NEP-0002 · the Rule of Two, as a check. This run holds all three legs at  # once: it reads a private file, ingests UNTRUSTED network content, and  # persists that content into the very file the NEXT run reads as trusted  # state. One human decision has to dominate EVERY path to that write — so the  # gate sits before the first network touch, not next to the write (a gate the  # fetch can route around dominates nothing). Blocking on purpose: a `default:`  # here would disarm it, and the checker knows — measured, adding `default:`  # flips TRIFECTA to `✖ NIKA-SEC-009 lethal trifecta complete`.  approve:    invoke:      tool: "nika:prompt"      args:        message: "Fetch ${{ const.source_url }} and persist it into ${{ const.state_path }}?"  previous_raw:    invoke:      tool: "nika:read"      args: { path: "${{ const.state_path }}" }    on_error:      on_codes: [NIKA-BUILTIN-READ-001]   # not-found ONLY · a permission error still fails loudly      recover: "[]"                       # first run · the STRING "[]", so `previous` parses it                                          # exactly like a real file — one code path, not two  previous:    with:      raw: ${{ tasks.previous_raw.output }}    invoke:      # `nika:read` hands back TEXT, and `nika:json_diff` compares VALUES. Feed      # it the raw string and the diff degrades to a single      # `{"op":"replace","path":""}` carrying the entire new document — it looks      # like it works, and it reports "everything changed" forever. Measured.      # `fromjson` is what makes the next task an actual diff.      tool: "nika:jq"      args:        input: "${{ with.raw }}"        expression: "fromjson"  fresh:    with:      go: ${{ tasks.approve.output }}   # the binding IS the edge · the gate dominates the fetch    when: ${{ with.go == true }}        # declined → no network touch at all    invoke:      tool: "nika:fetch"            # SLOT: fetch / read / exec · the fresh data      args:        url: "${{ const.source_url }}"        mode: jq        jq: ".records"    on_error:      recover: []                   # offline rehearsal · an empty batch, the delta stays quiet  delta:    with:                           # the bindings ARE the edges · previous + fresh → delta      previous: ${{ tasks.previous.output }}      fresh: ${{ tasks.fresh.output }}    invoke:      tool: "nika:json_diff"        # RFC 6902 · empty patch = nothing new      args:        before: "${{ with.previous }}"        after: "${{ with.fresh }}"  process:    with:      delta: ${{ tasks.delta.output }}    when: ${{ size(with.delta) > 0 }}    invoke:      tool: "nika:jq"               # SLOT: the delta job (jq · infer · write…)      args:        input: "${{ with.delta }}"        expression: "length"  save_state:    with:      fresh: ${{ tasks.fresh.output }}      go: ${{ tasks.approve.output }}   # the binding IS the edge · the gate dominates the write    when: ${{ with.go == true }}        # a refusal is a VALUE · the write is skipped, never failed    invoke:      tool: "nika:write"      args:        path: "${{ const.state_path }}"        content: "${{ with.fresh }}"    # a VALUE · the engine serializes it, and the next run's                                        # `fromjson` reads it straight back        create_dirs: true        overwrite: trueoutputs:  changes:    value: ${{ tasks.delta.output }}    description: "RFC 6902 ops since last run · empty = no-op run"

sha256 47f2985af82d7f52. The copy above re-hashes to its pin on every test run (a copy is re-provable, never trusted). Source: etl-state.nika.yaml in the spec pack · open it in the playground →

the patterns it locks

  • state read→parse→diff→write
  • `on_error: on_codes:` quarantine

Scaffold it locally: nika new etl-state my-flow.nika.yaml. Try the shape in the playground, or walk the showcase for the same patterns on real work. Read the spec →