Skip to main content
A Nika workflow is a list of tasks. Each task picks exactly one of four verbsinfer, exec, invoke, agent — and supplies its required fields. The set is closed: the parser’s verb dispatcher is exhaustive at compile time, so adding a fifth verb is a schema-version bump, not a patch release.
All four verbs execute end-to-end today — every snippet on this page is runnable with nika run at v. Verb grammar is frozen by the nika: v1 language envelope: these four, forever.

Why four?

A verb is a distinct native execution model the engine itself implements. Every workflow decomposes into one of these moves:
VerbOne-line intent
inferAsk an LLM.
execRun a local command.
invokeCall a builtin tool OR an MCP tool.
agentDelegate to an agent loop (LLM + tools + iteration budget).
Anything else we considered — “transform”, “map”, “branch”, “retry” — was a special case of one of these four, or workflow-level machinery (dependencies, for_each, when) that lives on the task envelope, not as a new verb. The set is closed at four. Everything callable is a tool under invoke; everything about ordering is a DAG construct on the task envelope.
Where did fetch go? Fetching a URL is calling a tool, not a distinct execution model — so it is the nika:fetch builtin, reached through invoke: (the extract modes become its mode argument). Same reason a DB query (invoke: mcp:postgres/query) or a file write (invoke: nika:write) is not its own verb.

exec — run a local command

Spawns a subprocess, captures stdout/stderr, returns exit code + output. Required: command (string). Optional: cwd (path), env (map · OS env for this subprocess), stdin (string), capture (stdout default · stderr · combined · structured). Task-level timeout: applies (not a verb field).
- name: build
  timeout: "300s"                  # task-level (applies to any verb)
  exec:
    command: cargo build --release
    cwd: ./crates/engine
    capture: structured            # { stdout, stderr, exit_code }
    env:
      RUSTFLAGS: "-C target-cpu=native"
Invariants (from nika-kernel::process::ShellExecutor):
  • Command::new MUST set kill_on_drop(true) — no leaked child processes (INV-011).
  • Concurrent pipe reading uses tokio::try_join! (INV-012).
  • Non-zero exit is a task failure (NIKA-EXEC-001) in the default capture modes — except capture: structured, where the exit code is data (output.exit_code · the task succeeds and the workflow branches on it). Sanitize untrusted interpolation through the shell_quote filter before the command reaches L1.
Cross-link: ShellExecutor trait (L0.5 kernel contract).

HTTP fetch — invoke: nika:fetch (a builtin, not a verb)

Fetching a URL is calling a tool, so it is the nika:fetch builtin reached through invoke: — not its own verb. Issues an HTTP request (GET/POST/etc.), returns bytes or parsed/extracted data; the old extract modes become the builtin’s mode argument.
- name: pull_readme
  timeout: "30s"                       # task-level (applies to any verb)
  invoke:
    tool: nika:fetch
    args:
      url: https://api.github.com/repos/supernovae-st/nika/readme
      method: GET
      mode: text                       # extraction mode · see extract-modes
      headers:
        Accept: application/vnd.github.raw
        Authorization: "Bearer ${{ env.GITHUB_TOKEN }}"
Invariants (from nika-kernel::http::HttpClient):
  • Output is tainted by default (external input is untrusted, T3:A).
  • Transient statuses (429, 5xx) are retried per retry: on the task.
  • Authentication headers never appear in events — they pass through SecretResolver + AuditSink::SecretRedacted.
See the builtins catalog for nika:fetch’s full argument schema (extract modes, session, cache).

invoke — call a builtin or MCP tool

Calls a builtin (nika: namespace) or a tool/resource exposed by a connected Model Context Protocol server. MCP server aliases are declared in the workflow’s mcp: block. Required: tool (string · nika:<path> for a builtin OR mcp:<server>/<tool> for an MCP tool). Optional: args (object · tool-specific schema). Task-level timeout: applies (not a verb field).
- name: search_vectordb
  invoke:
    tool: "mcp:chroma/query"          # mcp:<server>/<tool> · one colon, then a slash
    args:
      query: "${{ tasks.summarize.output }}"
      top_k: 5
The MCP server chroma is configured in the engine’s MCP server registry (engine config · out of scope of the workflow file). Builtins use the nika: namespace, e.g. tool: "nika:read". Routing goes through ToolExecutor + ToolExecute::execute(ToolCall) at L0.5 — the same trait the agent verb uses to dispatch in-loop tool calls (crates/nika-kernel/src/runtime/tool_executor.rs).

infer — LLM inference

Single-shot call to a language model. One InferRequest shape covers all providers in the catalog. Required: prompt (string). Optional: system (string), model (<provider>/<name> · overrides the workflow default), temperature (0.0–2.0), max_tokens (u32), schema (JSON Schema · structured output), thinking ({ enabled, budget_tokens }), vision (image inputs).
- name: summarize
  infer:
    model: mistral/<model>          # <provider>/<name> · see /reference/providers-catalog
    system: "You are a technical writer."
    prompt: |
      Summarize this in one sentence:
      ${{ tasks.pull_readme.output }}
    temperature: 0.3
    max_tokens: 500
Model identifiers are catalog-validated at parse time. See the Providers catalog for live IDs — we never hard-code them in concept docs because model names drift faster than the docs refresh cycle.
Invariants (from nika-kernel::provider::Provider):
  • Unified InferRequest (INV-017) — one DTO · per-provider wire dialects below (3 implemented today: anthropic · gemini · openai-compat — the openai-compatible dialect carries most of the 14-provider catalog · plus mock).
  • Capability rules ( rules) run before dispatch. Sending tools to a model whose capability rule says tool_calling = false fails with a typed validation error before the network round-trip.
  • Every provider emits the same InferEvent stream — Delta, ToolUseStart, ToolUseDelta, Thinking, Usage, Done.
Cross-link: Providers · Provider trait · ADR-035 (telemetry seams).

agent — multi-turn loop with tools

A model-driven loop: the agent calls tools, observes results, iterates until a completion signal fires or a budget exhausts. Required: prompt (string · initial user message). Optional: system (string), model (<provider>/<name>), tools (whitelist · glob patterns · default-deny), max_turns (u32 · default 10), max_tokens_total (u32 · cumulative token budget), temperature (0.0–2.0), schema (JSON Schema · validates the final message).
- name: investigate
  agent:
    model: mistral/<model>          # <provider>/<name>
    system: |
      You are investigating a GitHub issue. Use tools to gather
      context before answering.
    tools:                               # default-deny · grant explicitly
      - nika:fetch
      - mcp:browser/*
    max_turns: 10
    max_tokens_total: 100000
    prompt: |
      Investigate issue #${{ vars.issue_number }} and summarise
      the root cause in under 200 words.
Invariants (from nika-kernel::runtime::agent):
  • The loop is budget-bounded. The LANGUAGE contract (max_turns · max_tokens_total · spec 02-verbs) is what workflows declare; the engine’s AgentLoopConfig carries max_turns today plus its planning/reflection/compression knobs — max_tokens_total wires in with the runtime milestone. A ceiling hit is a typed failure (NIKA-AGENT-001 / NIKA-AGENT-002 · budget_error) with the last assistant message preserved in error.details.partial_output — recover it explicitly via on_error: if a partial is acceptable. Natural completion (or nika:done, optionally with result:) is success.
  • A failing tool call feeds its typed error back to the model (the loop continues against its budgets) — EXCEPT security_error, which fails the task immediately.
  • Every tool call is taint-checked — untrusted data cannot reach dangerous verbs without an explicit sanitizer.
  • The iteration trace (ToolCallRecord + CheckpointMessage) is emitted as events for audit and, optionally, checkpoint resume.
Cross-link: AgentLoopConfig · ADR-016 (cancellation model).

Verb selection is exhaustive

The nika-schema parser enforces “exactly one verb key per task” at compile time — the Verb enum in crates/nika-schema/src/parser/tasks.rs has four variants, and the action builder has exactly four match arms. Adding a fifth verb requires:
  1. Extending RawAction + the Verb enum.
  2. Wiring a new action builder arm.
  3. Bumping SchemaVersion (currently v1).
  4. Running all 12 admission gates on the consuming L2 verb crate.
This is the single hardest number in the Nika grammar to change. We’d rather say no.
Implementation status. Current verb executors ship as design-only kernel traits; the four L2 verb crates (nika-verb-infer, nika-verb-exec, nika-verb-invoke, nika-verb-agent) are all admitted and execute end-to-end (see Constellation). fetch is not a verb crate — it is the nika:fetch builtin reached through invoke. The verb grammar is locked and shipped (nika-schema).

Workflows

How verbs compose into a DAG.

Bindings

How values flow between tasks.

Providers

The -provider catalog behind infer and agent.

Events

Every verb emits typed events.