AGPL-3.0-or-later · forever.

the path · 04

Structured output + retry — an LLM call you can index into.

run itnika try 04-schema-retry

04-schema-retry.nika.yaml
# SPDX-License-Identifier: Apache-2.0# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json## 04 · Structured output + retry — an LLM call you can index into.## Without `schema:` a model hands back a paragraph and every downstream task# is string-surgery. With it, `.output` is an object: you write# `.output.entities` and the engine has already made that true, or failed# loudly trying. This is the line between a prompt and an API.## Demonstrates ·#   - `infer.schema:` · a JSON Schema the model MUST satisfy#   - `additionalProperties: false` · the shape is closed, not merely suggested#   - `retry:` · transient-error policy · attempts + backoff + jitter#   - typed `inputs:` with a `default:` · runs with no flags, `--var` overrides#   - indexing a structured result · `${{ tasks.extract.output.entities }}`#   - the long `outputs:` form · `value:` + `type:` + `description:`## Run · nika run examples/04-schema-retry.nika.yaml#   with your own text · nika run examples/04-schema-retry.nika.yaml \#     --var text="Marie Curie worked in Paris for the Sorbonne"nika: v1workflow:  id: schema-retrymodel: mock/echo   # the dry twin · swap `--model ollama/qwen3.5:4b` for a real extractionpermits: {}        # pure compute · no file, no host, no program (see 01)inputs:  text:    type: string    # A `default:` is what makes this file runnable with zero flags. `required`    # stays true: it says the task cannot proceed without a value, not that the    # caller must type one. `--var text=…` overrides the default.    default: "Ada Lovelace met Charles Babbage in London"    required: true    description: "Free text to extract named entities from"tasks:  extract:    infer:      prompt: "Extract named entities from · ${{ inputs.text }}"      max_tokens: 1024      schema:        type: object        # Closed on purpose. Without this a provider may add keys of its own        # and two providers give you two shapes for one workflow; the checker        # flags the open form as a strictness hint for exactly that reason.        additionalProperties: false        required: [entities]        properties:          entities:            type: array            items:              type: object              additionalProperties: false              required: [name, type]              properties:                name: { type: string }                type: { type: string, enum: [person, place, organization] }    retry:      # For TRANSIENT failures — a provider 503, a dropped connection. A      # response that misses the schema is a different animal: it is a      # validation error, and the engine's own re-ask handles that.      max_attempts: 3      backoff_strategy: exponential   # 1s · 2s · 4s …      jitter: true                    # anti-thundering-herd (the default)  report:    with:      # `.output` is an object here because `schema:` was set, so this indexes      # straight into it. Drop the schema and this same expression is reaching      # into a string.      entities: ${{ tasks.extract.output.entities }}    infer:      prompt: "Write one sentence summarizing these entities · ${{ with.entities }}"      max_tokens: 512# The long form · a typed return contract. `value:` carries the binding, and# `type:`/`description:` describe it for a caller reading the workflow's# signature rather than its body. Measured with the twin:#   {"entities":[{"name":"mock","type":"person"}],"summary":"mock(echo) · …"}outputs:  entities:    value: ${{ tasks.extract.output.entities }}    description: "The extracted named entities"  summary:    value: ${{ tasks.report.output }}    type: string    description: "One sentence covering them"

nika-spec@6ac29351a · sha256 0690693215bed7e8