Skip to main content
A workflow is a YAML document that names a goal, lists one or more tasks, and declares how values flow between them. Nika parses the file with source spans, validates it against the catalog, compiles it to a DAG, and executes tasks in topological order — parallelizing whatever the dependency graph allows.
Implementation status. The envelope below is the canonical spec contract (nika-spec · 01-envelope). The engine implements it end-to-end today — nika check parses and audits it, nika run executes it. Live state →

Minimal workflow

nika: v1
workflow: hello
description: "A minimal workflow."

tasks:
  - id: greet
    infer:
      prompt: "Say hi in one line."
      model: ollama/<model>      # <provider>/<name>
Two required lines (nika: + workflow:) and a non-empty tasks: — the whole minimum to be a valid workflow. Fields carry source spans through Spanned<T> so diagnostics point at the exact byte range.

Why one version marker (not a K8s envelope)

The envelope is one header line · nika: v1. Earlier drafts explored a Kubernetes-style apiVersion + kind + metadata + spec envelope, but the spec rejected it: that is two version-ish fields and ceremony a workflow file does not need. Modern specs converge on a single version marker — OpenAPI writes openapi: 3.1.0, Docker Compose dropped its version: field entirely. Nika takes the proven path: the language name as the key, the contract version as the value.
  • No separate kind: field — the presence of workflow: is itself the document-type discriminator. Future document types (if any ever ship) use their own top-level key.
  • The engine’s internal canonical URI stays https://nika.sh/spec/v1 for RDF / conformance tooling — but the author never types a URL.
Multi-document YAML is supported: one file may declare multiple documents separated by ---, parsed into Vec<RawDocument>.

Anatomy of a workflow

The workflow AST (nika-schema::raw::RawWorkflow) models these fields:
FieldTypePurpose
schemaSchemaVersionLocked to v1 today.
nameStringWorkflow identifier (within metadata: in the envelope).
descriptionStringHuman-readable purpose.
goalString (optional)Agent-driven workflow goal — moving to spec.orchestrate.goal per ADR-021.
provider / modelStringDefaults applied to every infer / agent task.
mcpRawMcpConfigMCP server aliases usable from invoke / agent.
contextContextConfigTop-level constants available to all tasks.
includeVec<IncludeSpec>Reusable sub-workflows inlined at parse time.
inputsVec<(String, Value)>Runtime parameters (--input key=value).
loggingLogConfigEvent verbosity per task kind.
orchestrateOrchestrateConfigAgentic orchestration (depth, concurrency).
routingRoutingConfigFallback + cost routing.
scheduleScheduleConfigCron / interval triggers.
max_duration_secsu64Hard wall-clock ceiling.
tasksVec<RawTask>The DAG payload.
Every optional field is Option<Spanned<T>> — source spans survive all the way into the diagnostic layer (ADR-010 miette bridge).

Execution pipeline

         ┌───────────────┐
 .yaml   │  nika-schema  │   RawWorkflow
  ─────► │     parser    │ ──────────────┐
         │ (marked-yaml) │               │
         └───────────────┘               ▼
                                 ┌────────────────┐
                                 │ nika-schema    │   DAG · typed
                                 │    analyzer    │   scope · taint
                                 │   (admitted)   │
                                 └────────────────┘


                                 ┌────────────────┐
                                 │  nika-runtime  │
                                 │  (L3 · live)   │
                                 │                │
                                 │ topo-sort ──►  │   ┌─ exec   L2
                                 │ dispatch  ──►  │   ├─ fetch  L2
                                 │ events    ──►  │   ├─ invoke L2
                                 │ checkpoint ──► │   ├─ infer  L2
                                 │                │   └─ agent  L2
                                 └────────────────┘


                                  EventSink · BillingSink · AuditSink
ASCII by intent — Mintlify renders fine, but the diagram above is plain text and pastes into terminals and issue trackers without a renderer.

Tasks, dependencies, fan-out

Tasks declare dependencies explicitly through depends_on:. Nika builds the DAG, topologically sorts, and runs tasks in parallel where the graph allows.
tasks:
  - name: fetch_data
    invoke:
      tool: nika:fetch
      args:
        url: https://example.com/data.json

  - name: analyze
    depends_on: [fetch_data]
    infer:
      model: ollama/<model>      # <provider>/<name>
      prompt: "${{ tasks.fetch_data.output }}"

  - name: notify
    depends_on: [analyze]
    exec:
      command: "echo ${{ tasks.analyze.output | shell_quote }}"
Fan-out over a collection:
- name: analyze_each
  depends_on: [list_files]
  for_each: "${{ tasks.list_files.output.lines }}"
  infer:
    model: ollama/<model>        # <provider>/<name>
    prompt: "Review this file: ${{ item }}"
Conditional execution — when: is the field (a CEL boolean, or the literal true for the always-pattern):
- id: deploy
  depends_on: [tests]
  when: "${{ tasks.tests.output.passed == true }}"
  exec:
    command: ./deploy.sh
Retry on failure — retry: is the one shape (and on_error: catches what retries can’t fix):
- id: flaky_api
  invoke:
    tool: nika:fetch
    args:
      url: https://example.com/api
  retry:
    max_attempts: 5
    backoff_strategy: exponential
    jitter: true
Field names are the spec’s: id: (not name:), when: (not condition:), retry.max_attempts (not max_retries:). The JSON Schema rejects the wrong names at check time.

Inputs · vars

Workflow inputs live in vars: (typed or untyped), available in every task via ${{ vars.<name> }}. Untyped constants and typed, validated inputs use the same block — the typed form powers schema generation for callable workflows (nika.run_workflow over MCP).
vars:
  target:                          # typed · validated + schema-gen
    type: string
    required: true
  verbose:
    type: boolean
    default: false
  model_family: claude             # untyped · the value is the default

tasks:
  - name: greet
    infer:
      model: ollama/<model>  # <provider>/<name>
      prompt: "Say hi to ${{ vars.target }}."
v1 workflows are single-file — there is no include: / import: (static composition is a candidate for a later additive minor · see the spec’s out-of-scope list). To reuse a sub-DAG today, call it with invoke: nika:run_workflow.
Runtime invocation:
nika run workflow.nika.yaml --input target=world

Outputs and events

Every task emits a structured Event stream during execution (nika-kernel::infra::event_sink::Event). Consumers subscribe via EventSink and receive one JSON object per line (NDJSON on stdout by default). See Events for the event model and Bindings for how the DAG propagates values.
The workflow grammar is locked and implemented: the nika-schema crate ships the envelope, the analyzer and semantic validation (type-checked bindings, secret-flow audit, cycle detection) — that is exactly what nika check runs on your file today.

Bindings

Template expressions — ${{ }} syntax, scopes, filters, taint.

Events

The typed event stream every run emits.

Verbs

The 4 verbs each task picks from.

YAML reference

Full schema specification (generated from AST).