learn · 5 minutes
One file, line by line.
Read one file. Understand everything.
A workflow is a file you can read, and nine small ideas make you fluent in it. Every fragment below is real, spec-correct YAML, read through the same editor surface you'll use in the playground.
9 steps · spec-correct
- 01 · the file
A workflow is a file you can read
The whole thing is one plain-text file. Two lines make it real: name the language, name the workflow. That header is the whole ceremony: no project setup, no boilerplate, no config.
nika: v1 means the format is frozen. Files you write today won’t break.
weekly-radar.nika.yamlnika: v1workflow: id: weekly-radar - 02 · the inputs
Declare what can change
Inputs live in vars. A bare value is a default you can override from the command line; a typed var documents itself and gets validated before anything runs.
Use it anywhere as ${{ vars.topic }}. Change the input, not the file.
varsvars: output_dir: "./radar" topic: type: string required: true description: "Subject to research"Next week the topic changes. What do you edit?
- 03 · the model
Pick a brain. Any brain.
One line chooses the default model, any model: local Ollama, or any API. Start on your own machine (no key, no cloud) and swap providers whenever you want; nothing else changes.
model# fully local · no cloud neededmodel: ollama/llama3.2:3b# or swap to any cloud provider:# model: mistral/mistral-large - 04 · the verbs
A task is a verb
Each task does exactly one thing, with one of the four verbs. This one thinks: it sends a prompt to the model and keeps the answer as its output.
infer thinks · exec runs a command · invoke uses a tool · agent delegates.
taskstasks: digest: after: fetch_news: succeeded infer: prompt: "Summarize in 5 bullets: ${{ tasks.fetch_news.output }}" - 05 · the plan
The wiring is the plan. The plan is free.
with: names what a task takes in — and each wire IS an edge of the plan. Tasks that don’t feed each other run in parallel automatically. You never schedule anything. The plan (which tasks wait on which) falls out of the file.
fetch_news and repo_log run at the same time. digest waits for both. Order with no data is the same declaration: depends_on works with or without a with: binding.
withfetch_news: invoke: tool: "nika:fetch"repo_log: exec: command: ["git", "log", "--since=1 week"]digest: with: news: ${{ tasks.fetch_news.output }} # each wire in log: ${{ tasks.repo_log.output }} # is one edge infer: prompt: "Cross-reference ${{ with.news }} with ${{ with.log }}…"digest reads ${{ tasks.fetch_news.output }} in with:. What did that line just do?
- 06 · the waves
Steps that wait, steps that run together
A workflow is a to-do list where some steps wait for others. Steps that wait on nothing all start at the same time, automatically; you never schedule anything. Before anything runs, the runtime reads every with: wire and draws the plan: here, three sources start together, the digest waits for all three, and the save waits for the digest.
Nothing in this file says parallel. The picture below is the plan drawn from these five steps: follow the arrows, not the line order.
tasks · the whole plantasks: fetch_news: invoke: tool: "nika:fetch" repo_log: exec: command: ["git", "log", "--since=1 week"] read_notes: invoke: tool: "nika:read" digest: after: fetch_news: succeeded repo_log: succeeded read_notes: succeeded with: news: ${{ tasks.fetch_news.output }} log: ${{ tasks.repo_log.output }} notes: ${{ tasks.read_notes.output }} infer: prompt: "One weekly radar, five bullets" save: after: digest: succeeded with: brief: ${{ tasks.digest.output }} invoke: tool: "nika:write"the plan the runtime draws from this exact file · every arrow points from a step to the step that waits for it - 07 · the branch
Branch like an adult
when: makes a task conditional, a yes/no test over what it imports. The wiring already orders it; when: decides whether an admitted step runs — and it reads the step’s own bindings, never the graph.
whenalert: with: errors: ${{ tasks.check.output.errors }} when: ${{ with.errors > 0 }} invoke: tool: "nika:notify"check failed outright. What happens to alert and its when: test?
- 08 · the failure
When things fail, you get data
Errors come back typed: a stable code, a category, and whether retrying could help. Tasks declare their own retry policy and a fallback. No stack-trace archaeology.
A failed call retries with backoff; if it still fails, the cached result steps in.
retry · on_errorresearch: retry: max_attempts: 3 backoff_ms: 1000 on_error: recover: ${{ tasks.cache.output }} infer: prompt: "…"The error says transient: false. What does retrying buy you?
- 09 · the outputs
Name what comes out
output: binds pieces of a task result to names; the workflow declares what it returns. Downstream tasks (and you) read clean names, not raw API responses.
output · outputstasks: digest: infer: prompt: "…" output: result: ".choices[0].message.content"outputs: brief: ${{ tasks.digest.output.result }}
Errors are data, not noise.
typed · greppableEvery failure is a typed structure with a stable code, a category, and a transient flag that says whether retrying could help. Your workflow can read errors the same way it reads any other value, and recover.
{ "code": "NIKA-INFER-001", "category": "provider_error", "message": "the model call failed", "transient": true, "details": { "provider": "ollama", "status_code": 503, "retry_after_secs": 30 }, "task_id": "research", "attempt": 2}codea stable, greppable identifier. The same failure always has the same name.transienttrue means retry might work. The engine retries with backoff before giving up.detailsstructured fields, not prose. Youron_error:can act on them.
10 · the whole file
Every idea above, in one file
The nine fragments compose into the workflow this page has been teaching. This exact text passes the engine's audit; the verdict below is nika check's real answer, and its hints are your next three lessons.
nika: v1workflow: id: weekly-radarvars: output_dir: "./radar" topic: type: string required: true description: "Subject to research"model: ollama/llama3.2:3btasks: fetch_news: invoke: tool: "nika:fetch" args: url: "https://hnrss.org/frontpage" repo_log: exec: command: ["git", "log", "--since=1 week"] read_notes: invoke: tool: "nika:read" args: path: "./notes.md" digest: with: news: ${{ tasks.fetch_news.output }} log: ${{ tasks.repo_log.output }} notes: ${{ tasks.read_notes.output }} retry: max_attempts: 3 backoff_ms: 1000 infer: prompt: "One weekly radar on ${{ vars.topic }}, five bullets: ${{ with.news }} ${{ with.log }} ${{ with.notes }}" save: with: brief: ${{ tasks.digest.output }} invoke: tool: "nika:write" args: path: "${{ vars.output_dir }}/radar.md" content: "${{ with.brief }}"outputs: brief: ${{ tasks.digest.output }}nika check weekly-radar.nika.yamlnika check · weekly-radar.nika.yaml ✔ PLAN 3 waves · 5 tasks · max parallelism 3 wave 1 fetch_news (invoke · nika:fetch) · repo_log (exec · git) · read_notes (invoke · nika:read) wave 2 digest (infer · ollama/llama3.2:3b) wave 3 save (invoke · nika:write) ✔ MODELS 1 model resolves in this binary ⚠ COST $0.0000 – $0.0000 FLOOR (unbounded tasks present) digest ollama/llama3.2:3b UNBOUNDED — no max_tokens declared ✔ SECRETS no information-flow escapes ✔ TYPES every deep output reference fits its declared shape ✔ TOOLS every nika: tool names a canonical builtin ✔ ARGS every invoke arg key is declared + every required arg is present ✔ SCHEMA every authored schema: is satisfiable ○ PERMITS no boundary declared (engine floor only) · `--infer-permits` writes one ↳ HINT [cost] declare `max_tokens` on `digest` — the cost report becomes a hard ceiling instead of UNBOUNDED ↳ HINT [permits] no `permits:` boundary declared — run `nika check --infer-permits` to generate the tightest one (default-deny once present) ↳ HINT [inputs] `read_notes` reads `./notes.md` which does not exist here — create it (or point its var elsewhere) · the run would fail at that wave ↳ HINT [inputs] required input(s) with no default · pass at run time: --var topic=… ✔ audited · 5 tasks · 3 waves · permits none · est ≥$0.0000 · 4 hints
real output · nika 0.105.0 · nika check weekly-radar.nika.yaml · re-captured at every release
That's the whole language.
Nine ideas, four verbs, one file. Install it, write one, run it, or open the playground and check your file as you type. Or send us the task you repeat and get it back as a file.
9 steps · 4 verbs · every fragment spec-correct · real YAML, never pseudo-code