> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nika.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# The 4 verbs

> infer · exec · invoke · agent. Four verbs, closed set, typed contracts. Everything else is data.

export const STATUS = {
  head: "95962d5cd",
  branch: "main",
  version: "0.91.0",
  cratesWorkspace: 39,
  cratesAdmitted: 39,
  cratesTarget: "42",
  wipCrates: [],
  libTests: 2989,
  clippyWarnings: 0,
  adrs: 62,
  adrsAccepted: 42,
  adrsProposed: 18,
  providers: 32,
  capabilityRules: 49,
  hygieneVectors: 38,
  hygieneGreen: 28,
  hygieneYellow: 3,
  hygieneRed: 0,
  lastUpdated: "2026-06-25"
};

A Nika workflow is a list of **tasks**. Each task picks exactly one of
**four verbs** — `infer`, `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.

<Info>
  All four verbs execute end-to-end today — every snippet on this page is
  runnable with `nika run` at v{STATUS.version}. Verb grammar is frozen by
  the `nika: v1` language envelope: these four, forever.
</Info>

## Why four?

A verb is a **distinct native execution model** the engine itself
implements. Every workflow decomposes into one of these moves:

| Verb     | One-line intent                                             |
| -------- | ----------------------------------------------------------- |
| `infer`  | Ask an LLM.                                                 |
| `exec`   | Run a local command.                                        |
| `invoke` | Call a builtin tool OR an MCP tool.                         |
| `agent`  | Delegate 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.

```mermaid theme={"system"}
%%{init: {'theme':'dark','themeVariables':{'background':'transparent','mainBkg':'transparent'}}}%%
flowchart LR
    classDef verb fill:#3F7DFF,color:#fff,stroke:#7FE9FF,font-weight:bold
    classDef tool fill:#1f2937,color:#94a3b8,stroke:#475569
    classDef out fill:#0b1530,color:#7FE9FF,stroke:#3F7DFF

    Y["📄 .nika.yaml<br/>intent"]:::out --> D{verb?}
    D -->|"think"| I["infer<br/>LLM call"]:::verb
    D -->|"run"| E["exec<br/>subprocess"]:::verb
    D -->|"use"| V["invoke<br/>tool dispatch"]:::verb
    D -->|"delegate"| A["agent<br/>loop + budget"]:::verb
    V --> B["nika:* builtins"]:::tool
    V --> M["MCP servers"]:::tool
    A -.->|"each turn may"| V
    I & E & V & A --> R["typed output<br/>tasks.X.output"]:::out
```

<sub>The set is closed at four. Everything *callable* is a tool under `invoke`; everything about *ordering* is a DAG construct on the task envelope.</sub>

> **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).

```yaml theme={"system"}
- 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](/architecture/layers#layer-table) (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.

```yaml theme={"system"}
- 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](/reference/constellation) 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).

```yaml theme={"system"}
- 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 {STATUS.providers} 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).

```yaml theme={"system"}
- 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
```

<Note>
  Model identifiers are catalog-validated at parse time. See the
  [Providers catalog](/reference/providers-catalog) for live IDs — we
  never hard-code them in concept docs because model names drift faster
  than the docs refresh cycle.
</Note>

**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 ([{STATUS.capabilityRules} rules](/reference/capabilities))
  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](/concepts/providers) · [`Provider` trait](/architecture/layers#layer-table) · 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).

```yaml theme={"system"}
- 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`](/architecture/layers#layer-table) · 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.

<Info>
  **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](/reference/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`).
</Info>

## Read next

<CardGroup cols={2}>
  <Card title="Workflows" icon="diagram-project" href="/concepts/workflows">
    How verbs compose into a DAG.
  </Card>

  <Card title="Bindings" icon="link" href="/concepts/bindings">
    How values flow between tasks.
  </Card>

  <Card title="Providers" icon="server" href="/concepts/providers">
    The {STATUS.providers}-provider catalog behind `infer` and `agent`.
  </Card>

  <Card title="Events" icon="signal" href="/concepts/events">
    Every verb emits typed events.
  </Card>
</CardGroup>
