inferCall a modelCall a model. Any of the providers; structured output when you give it a schema.
research: infer: prompt: "Research ${{ vars.topic }}"the language reference
The contract. Frozen forever.
A .nika.yaml is the plan, written down: one envelope, four verbs, a typed task shape, and a permits block that declares, and bounds, everything it's allowed to touch. You review it, the runtime enforces it, then it runs. This is the friendly map; the nika-spec repository is the canonical, normative source.
tl;dr · 5 pillars
nika: v1infer · exec · invoke · agentwith · after${{ }}NIKA-*full text: github.com/supernovae-st/nika-spec ↗ · machine contract: workflow.json
Every file opens with nika: v1, one version marker, pinned for the v1 lifetime. No v1.0, no v2 migration. Three keys are required; the rest are optional.
nikarequiredthe version marker · exactly v1, foreverworkflowrequiredthe workflow id · kebab-case · unique in the filetasksrequiredthe DAG · one or more nodes, each binding one verbdescriptionoptionala human note · free textmodeloptionalthe default model · provider/name (e.g. ollama/llama3.2:3b)varsoptionaltyped inputs · ${{ vars.X }} · with required / defaultenvoptionalnon-sensitive runtime config · ${{ env.X }}secretsoptionalvault-backed references · never inline literalspermitsoptionalthe capability boundary · default-deny once presentoutputsoptionalthe return value · ${{ tasks.X.output }} · symmetric to varsA real file · standup-digest
nika: v1workflow: id: standup-digest description: "Read yesterday's commits, write today's standup note"model: ollama/qwen3.5:4b # local · zero key · swap for anthropic/claude-haiku-4-5 (fast one-liner job)tasks: # No deps between these two → the engine runs them in parallel. today: invoke: tool: "nika:date" args: { op: now } history: exec: command: ["git", "log", "--since=yesterday", "--oneline", "--no-merges"] digest: with: today: ${{ tasks.today.output }} history: ${{ tasks.history.output }} infer: prompt: | Date · ${{ with.today }} Commits since yesterday · ${{ with.history }} Write my standup note · 3 bullets · done / doing / blocked. Plain words · no fluff. save: with: digest: ${{ tasks.digest.output }} invoke: tool: "nika:write" args: path: "./standup-note.md" content: "${{ with.digest }}"outputs: note: ${{ tasks.digest.output }}A verb is a distinct native execution model. A task binds exactly one. That is the whole operation space: fetch, recall, db and files are tools reached under invoke:, not verbs.
inferCall a modelCall a model. Any of the providers; structured output when you give it a schema.
research: infer: prompt: "Research ${{ vars.topic }}"execRun a processRun a real process. stdout becomes the output; a non-zero exit becomes an error.
build: exec: command: ["cargo", "build", "--release"]invokeCall a toolCall a tool · a nika: builtin or an mcp: server. Default-deny, args schema-checked.
read_config: invoke: tool: "nika:read" args: path: ./config.yamlagentDrive a loopDrive an autonomous tool-use loop, bounded by max_turns and a whitelist of tools.
research: agent: prompt: "Research ${{ vars.topic }}" tools: ["nika:fetch"]A task is a DAG node. Its map key is its identity and exactly one verb binds;tasks.* crosses the boundary through two doors — with (data) and after (control); everything else is an optional structural control.
required coreoptional controls
‹key›reqthe map key IS the identity · snake_case · CEL-safe · unique‹verb›reqexactly one of infer · exec · invoke · agentwithoptthe data door · each ${{ tasks.X }} binding IS a typed edgeafteroptthe control door · {producer: success|failure|skipped|terminal}whenopta local CEL yes/no (or true/false) · post-gate · false → skippedfor_eachoptmap the task over a collectionmax_paralleloptcap concurrent for_each iterations · 1 = sequentialtimeoutopta quoted Go-duration · e.g. "30s" "5m" "1h30m" · max 24hretryoptattempts + backoff on a transient failureon_erroroptfallback · recover from another task · or continueThe capability boundary: the contract an agent must satisfy before it acts. Once permits: is present, every category is default-deny: which files it can read, which it can write (read XOR write), which hosts it can reach, which programs it can run, which tools it may call. The runtime enforces it: out of bounds is denied, not logged after the fact.
fs.read / fs.writenet.httpexectoolsfs.read / fs.writenet.httpexectoolsdenied, before it runs
See it felt, not told: toggle a permit and watch the runtime obey.
28 builtin tools, all reached with invoke: · nothing to install. Grouped by what they touch.
Files· 5
readwriteeditglobgrepData· 10
jqconvertvalidatejson_diffjson_merge_patchcomposehashuuiddatechartWeb· 1
fetchMedia· 3
image_generatetts_generateimage_fxFlow· 9
assertdonewaitemitlognotifypromptinspectdecidePick per task or per file. Local-first: provider: ollama runs offline, no key. Cloud is open-weight-first (Mistral leads).
Local runtimes· 5 · offline
OllamaLM Studiollama.cppLocalAIvLLMCloud · open-weight first· 11
MistralAnthropicOpenAIGeminiDeepSeekxAIGroqOpenRouterHugging FaceNVIDIAmoonshotTest· 1 · deterministic
mockHow nika:fetch turns a page into typed output: from raw text to a parsed article, feed or jq projection.
articlefeedjqlinksmarkdownmetadataselectorsitemaptextEvery failure carries a typed code (NIKA-‹NS›-NNN) across 21 namespaces, with a category and a transient flag. The full registry lives in errors/catalog.json.
NIKA-AGENTagent: the loop budgetNIKA-ASSERTsee the error catalogNIKA-BUILTINbuiltin tool argument contractsNIKA-CANCELtask or workflow cancellationNIKA-COMPsee the error catalogNIKA-DAGDAG topology · cycles · invalid depsNIKA-EXECexec: the local processNIKA-IMPLengine-internal errorsNIKA-INFERinfer: the model callNIKA-INVOKEinvoke: the tool callNIKA-LOCKsee the error catalogNIKA-MCPMCP client + transportNIKA-DECIDEsee the error catalogNIKA-PARSEYAML parse + envelope validationNIKA-POLICYsee the error catalogNIKA-PORTsee the error catalogNIKA-PROVIDERprovider adapter failuresNIKA-SECsecurity policy · SSRF · whitelist · permitsNIKA-TIMEOUTtask or step timeoutsNIKA-TYPEsee the error catalogNIKA-VARvariable + expression resolutionThe contract you can count on: the parts that never change.
S.8aReal semver toward 1.0
The engine ships on real semver: currently v0.105.0, toward a 1.0 launch. The language envelope stays nika: v1, frozen forever.
S.8bFour verbs, locked
infer · exec · invoke · agent: a closed set, locked forever. New capability arrives as a tool, never a fifth verb.
S.8cThe spec is Apache-2.0
The language spec is permissive: adopt it, build a runtime against it, with a patent grant. The standard for the workflow file.
S.8dThe engine is AGPL-3.0-or-later
Copyleft on the engine: a hosted fork shares its source. Anti-extraction by construction.
PROTOCOL
The same contract, everywhere you meet it: the chapters are normative, the schema is what your editor validates, the engine enforces, the oracle teaches your agent, the registry re-proves what you share.
4 verbs · 28 builtins · 17 providers · 9 extract modes · 21 error namespaces · every count derives from the spec's canon.yaml, never hand-typed
nika
Intent as Code. The contract, frozen forever.