FIG L · learn · 5 minutes

One file, line by line.

A workflow is a file you can read — and eight 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.

8 steps · spec-correct

  1. 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.yaml
    nika: v1workflow: weekly-radar
  2. 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.

    vars
    vars:  output_dir: "./radar"  topic:    type: string    required: true    description: "Subject to research"
  3. 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.1# or swap to any cloud provider:# model: mistral/mistral-large
  4. 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.

    tasks
    tasks:  - id: digest    infer:      prompt: "Summarize in 5 bullets: ${{ tasks.fetch_news.output }}"
  5. 05 · the plan

    Order is one word. The plan is free.

    depends_on is all you write. Tasks that don’t wait on 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.

    depends_on
    - id: fetch_news  invoke:    tool: "nika:fetch"- id: repo_log  exec:    command: "git log --since='1 week'"- id: digest  depends_on: [fetch_news, repo_log]   # waits for BOTH  infer:    prompt: "Cross-reference news with our work…"
  6. 06 · the branch

    Branch like an adult

    when: makes a task conditional — a yes/no test over what already happened. Waiting for success is free (depends_on already does it); when: is for conditions beyond it, like a value check.

    when
    - id: alert  depends_on: [check]  when: ${{ tasks.check.output.errors > 0 }}  invoke:    tool: "nika:notify"
  7. 07 · 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_error
    - id: research  retry:    max_attempts: 3    backoff_ms: 1000  on_error:    recover: ${{ tasks.cache.output }}  infer:    prompt: "…"
  8. 08 · 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 · outputs
    tasks:  - id: digest    infer:      prompt: "…"    output:      result: ".choices[0].message.content"outputs:  brief: ${{ tasks.digest.output.result }}
09 · the failure object

Errors are data, not noise.

typed · greppable

Every 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.

error.json
{  "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. Your on_error: can act on them.

That's the whole language.

Eight ideas, four verbs, one file. Install it, write one, run it — or open the playground and check your file as you type.

8 steps · 4 verbs · every fragment spec-correct — real YAML, never pseudo-code