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 handapproveinvokeprevious_rawinvoke
previousinvokefreshinvoke
deltainvokesave_stateinvoke
processinvoke
the file, whole
conformance-gated upstream on every spec pushetl-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
cross-references
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 →