{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://nika.sh/spec/v1/workflow.schema.json",
  "title": "Nika workflow · v1",
  "description": "Structural contract for a Nika v1 workflow file. Hand-derived from the prose spec (spec/01-08) at v0.1 · to be superseded by the engine-generated `nika-schema` (schemars) output at engine GA · both derive from the same prose source of truth. Strict on structure (envelope · exactly-one-verb-per-task · enums · id patterns · unknown-key rejection) · permissive on leaf value types where a `${{ }}` CEL template may appear in place of a literal.",
  "$comment": "INTERIM hand-derived schema · prose spec is the single source of truth · regenerate + diff against engine `nika-schema` at GA.",
  "type": "object",
  "required": [
    "nika",
    "tasks"
  ],
  "additionalProperties": false,
  "properties": {
    "nika": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9-]*$",
      "description": "The file's NAME · kebab-case · and the mark that says « this is a nika file ». One word, one meaning across both document types: `nika.yaml` carries the PROJECT name, a `*.nika.yaml` carries the WORKFLOW name. The document TYPE is read from `tasks:` — present means workflow, absent means project — so it survives a blob, a paste or an HTTP body where the filename does not travel. This key held the literal `v1` until the envelope nuke; it carried zero bits as a version (there is no `nika: v2` — ever) and now carries the file's most necessary field instead."
    },
    "model": {
      "type": "string",
      "description": "Default model · `<provider>/<name>` (e.g. ollama/qwen3.5:9b · anthropic/claude-sonnet-4-6 · mock/echo)."
    },
    "inputs": {
      "type": "object",
      "description": "Typed workflow inputs · `${{ inputs.X }}` · the parameters an author declares and a caller supplies. Each entry is a typed declaration whose `type:` speaks the full TypeExpr of 09-types (R3b · LAW-GRAMMAR-0211 · the flat 6-enum is dead · LAW-SURFACE-0211). The typed half of the dead `vars:` block (R3a · LAW-SURFACE-0201) · a `required: true` value lands here per the E-split total rule.",
      "additionalProperties": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "$ref": "#/$defs/typeExpr"
          },
          "required": {
            "type": "boolean",
            "description": "The caller MUST supply a value (no default is consulted)."
          },
          "default": {
            "description": "The value used when the caller supplies none · MUST conform to `type:` (R3b · LAW-TYPE-0211 · NIKA-DEFAULT-001 · the P0 soundness hole is closed at check)."
          },
          "description": {
            "description": "Free-form prose about this input · what a caller is being asked for. Documentation only — the contract is `type` and `required`.",
            "type": "string"
          }
        }
      }
    },
    "const": {
      "type": "object",
      "description": "Named constants · `${{ const.X }}` · a fixed value baked into the workflow. Either a bare literal, or a `{ type, value }` typed constant whose `value:` MUST conform to `type:`. The literal half of the dead `vars:` block (R3a · LAW-SURFACE-0201) · everything that is not `required: true` lands here per the E-split total rule.",
      "additionalProperties": {
        "type": [
          "string",
          "number",
          "boolean",
          "array",
          "object",
          "null"
        ],
        "if": {
          "type": "object",
          "required": [
            "type",
            "value"
          ]
        },
        "then": {
          "additionalProperties": false,
          "properties": {
            "type": {
              "$ref": "#/$defs/typeExpr"
            },
            "value": {
              "description": "The constant value · MUST conform to `type:` (NIKA-DEFAULT-001)."
            },
            "description": {
              "description": "Free-form prose about this constant · why the value is what it is. Documentation only — the contract is `type` and `value`.",
              "type": "string"
            }
          },
          "$comment": "An object carrying BOTH `type` and `value` is a typed constant · the type speaks the full TypeExpr (R3b) and values_core checks the value against it (NIKA-DEFAULT-001) · an object missing either key is a bare literal object constant."
        }
      }
    },
    "secrets": {
      "type": "object",
      "description": "Vault-backed masked references · `${{ secrets.X }}` · never inline literals.",
      "additionalProperties": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "source"
        ],
        "properties": {
          "source": {
            "enum": [
              "vault",
              "env",
              "file"
            ],
            "description": "Where the secret lives · never an inline value (spec/01-envelope.md §secrets)."
          },
          "key": {
            "type": "string",
            "description": "Store key (vault) or OS env var name (env)."
          },
          "path": {
            "type": "string",
            "description": "File path · file source only · contents read at resolve time · masked."
          },
          "egress": {
            "type": "array",
            "description": "Sanctioned destinations for this secret · declassification (spec/01-envelope.md §egress) · absent/empty = default-deny (every exec:/invoke: reach is a leak).",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "to"
              ],
              "properties": {
                "to": {
                  "type": "string",
                  "description": "The sanctioned sink · a tool id (`nika:fetch` · `nika:notify` · `mcp:<server>/<tool>`), `exec`, a provider-egress sink `infer` / `agent` (a secret in an infer/agent prompt), or `outputs` (the workflow boundary — a return value derived from the secret's response) · SPECIFIC (no cross-tool laundering)."
                },
                "host": {
                  "type": "string",
                  "description": "Static-literal destination host · sanctions only when the sink's destination arg is exactly this host (a templated host stays the runtime check). Mutually exclusive with host_from_self."
                },
                "host_from_self": {
                  "type": "boolean",
                  "description": "The secret value IS the destination URL (host unknown statically) · sanctions only the direct-secret-URL shape with the non-occlusion guard. Mutually exclusive with host."
                }
              },
              "not": {
                "required": [
                  "host",
                  "host_from_self"
                ]
              }
            }
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "source": {
                  "const": "file"
                }
              }
            },
            "then": {
              "required": [
                "path"
              ],
              "not": {
                "required": [
                  "key"
                ]
              }
            },
            "else": {
              "required": [
                "key"
              ],
              "not": {
                "required": [
                  "path"
                ]
              }
            }
          }
        ],
        "description": "A secret is a reference to a store · discriminated by source · vault/env require key · file requires path · optional egress: sanctioned-destination list (spec/01-envelope.md)."
      }
    },
    "permits": {
      "type": "object",
      "additionalProperties": false,
      "description": "The declared capability boundary · once present every category is default-deny unless listed (spec/01-envelope.md §permits · NIKA-SEC-004).",
      "properties": {
        "fs": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "read": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "write": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        },
        "net": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "http": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        },
        "exec": {
          "description": "false = no shells · true = any (blocklist-gated) · array = allowed program names.",
          "oneOf": [
            {
              "type": "boolean"
            },
            {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          ]
        },
        "tools": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Allowed nika:/mcp: tool ids · globs ok."
        },
        "env": {
          "type": "array",
          "items": {
            "type": "string",
            "anyOf": [
              {
                "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
              },
              {
                "pattern": "\\$\\{\\{"
              }
            ]
          },
          "description": "Engine env names passed through to child processes · exact POSIX names, no globs · composed with the runner env floor, never inherited (spec/01-envelope.md §permits · NEP-0005). An interpolated entry passes the shape gate and is refused as a non-literal bound (NIKA-AUTH-007)."
        }
      }
    },
    "run": {
      "type": "object",
      "additionalProperties": false,
      "description": "The run's entropy and clock declaration · every source of randomness and of time is declared, never ambient. The two dimensions couple: only `ambient × system` (the status quo) and `none | seeded × virtual` (the deterministic states) are legal, and a declared contradiction refuses at parse (NEP-0010).",
      "properties": {
        "entropy": {
          "description": "none (strict determinism) · ambient (the honest status quo, default) · { seeded: <u64> } (the deterministic seams + the pinned seed · two runs of the same file with the same seed produce byte-identical journals)",
          "anyOf": [
            {
              "enum": [
                "none",
                "ambient"
              ]
            },
            {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "seeded": {
                  "type": "integer",
                  "minimum": 0,
                  "description": "The run's pinned seed (u64)"
                }
              },
              "required": [
                "seeded"
              ]
            }
          ]
        },
        "clock": {
          "enum": [
            "system",
            "virtual"
          ],
          "description": "system (wall time, default) · virtual (the injected virtual clock · time becomes a contract input · a timeout: budget reads against it)"
        }
      }
    },
    "tasks": {
      "type": "object",
      "minProperties": 1,
      "propertyNames": {
        "pattern": "^[a-z][a-z0-9_]*$"
      },
      "additionalProperties": {
        "$ref": "#/$defs/task"
      },
      "description": "The task map · the KEY is the task's identity (snake_case · CEL-safe). Source order is presentation only — the graph alone schedules."
    },
    "outputs": {
      "type": "object",
      "description": "The workflow's return value · symmetric to inputs. Each entry is a `${{ tasks.X.output }}` reference (untyped form · string) OR a typed declaration { value · type · description }. Powers `nika run` result + the output half of the callable-workflow schema.",
      "additionalProperties": {
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "object",
            "required": [
              "value"
            ],
            "additionalProperties": false,
            "properties": {
              "value": {
                "type": "string"
              },
              "type": {
                "$ref": "#/$defs/typeExpr",
                "description": "The output's type · the full TypeExpr of 09-types (R3b · LAW-GRAMMAR-0211 · the flat 6-enum is dead alongside the inputs half · LAW-SURFACE-0211)."
              },
              "description": {
                "description": "Free-form prose about this output · what a caller receives. Documentation only — the contract is `value` and `type`.",
                "type": "string"
              }
            }
          }
        ]
      }
    }
  },
  "$defs": {
    "task": {
      "type": "object",
      "additionalProperties": false,
      "description": "A DAG node. MUST bind exactly one of the 4 verbs (infer · exec · invoke · agent).",
      "allOf": [
        {
          "oneOf": [
            {
              "required": [
                "infer"
              ]
            },
            {
              "required": [
                "exec"
              ]
            },
            {
              "required": [
                "invoke"
              ]
            },
            {
              "required": [
                "agent"
              ]
            }
          ]
        }
      ],
      "properties": {
        "group": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9_]*$",
          "description": "Fan-in MEMBERSHIP · this task joins the named group, and a consumer folds the whole group with one `${{ group.<name> }}` binding in its `with:` (spec/03-dag.md §group). Membership is DECLARED, never matched: a renamed member leaves its group loudly (NIKA-DAG-008 on the reference), where a glob would shrink the fold in silence. A group exists iff at least one task declares it. An `unwind` task may not join one (NIKA-DAG-009 · cleanup never schedules)."
        },
        "after": {
          "type": "object",
          "description": "The CONTROL boundary · {producer-task: predicate}. Each entry is one control edge (spec/03-dag.md §after). State, never data — observe outcomes through with:.",
          "additionalProperties": false,
          "patternProperties": {
            "^[a-z][a-z0-9_]*$": {
              "type": "string",
              "enum": [
                "success",
                "failure",
                "skipped",
                "terminal",
                "unwind"
              ],
              "description": "The producer states that admit this task · terminal = any settled state INCLUDING cancelled (spec/03-dag.md §gate algebra v2). `unwind` is NOT a settle-state comparison · it is the E_f cleanup attachment (spec/03-dag.md §unwind) · it fires on cancel and timeout for a producer that STARTED, runs before the failure settles outward, never enters G_p, and its own failure does not propagate."
            }
          }
        },
        "when": {
          "oneOf": [
            {
              "type": "boolean",
              "description": "YAML boolean literal · when: false is the never-run switch (when: true restates the default · lint)."
            },
            {
              "type": "string",
              "format": "cel-expression",
              "description": "A ${{ }} CEL boolean expression (cel-subset/0.1 · spec/03-dag.md). Statically non-boolean-shaped roots are rejected (NIKA-VAR-005)."
            }
          ],
          "description": "LOCAL business condition · false skips the task · evaluated POST-gate over {vars · env · with · item · index}. Referring to `tasks.*` is illegal here (NIKA-VAR-021 · hoist it into `with:`) · spec/03-dag.md §when."
        },
        "for_each": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "items"
          ],
          "description": "Fan this task out over a collection · ONE block, so the concurrency is visible where the fan-out is declared. `max_parallel` and `fail_fast` live here because they have no meaning without it.",
          "properties": {
            "items": {
              "type": [
                "string",
                "array"
              ],
              "description": "The collection · a `${{ ... }}` reference OR a literal array. Evaluated EXACTLY ONCE, before the fan-out; no iteration feeds back into it (spec 03 §where this sits)."
            },
            "max_parallel": {
              "type": "integer",
              "minimum": 1,
              "description": "Cap concurrent iterations · default unbounded · 1 = sequential."
            },
            "fail_fast": {
              "type": "boolean",
              "description": "Whether the fan-out abandons the batch on the first failure · default true. False finishes every item and yields null at a failed index, so the batch reports what it could."
            }
          }
        },
        "retry": {
          "description": "Retry policy for this task · `{ max_attempts, backoff_ms, backoff_strategy, backoff_max_ms, jitter, on_codes }`. Retries run BEFORE `on_error` sees anything — the catch handles the last error only (spec/05-errors.md §Retry policy).",
          "$ref": "#/$defs/retry"
        },
        "on_error": {
          "$ref": "#/$defs/onError"
        },
        "timeout": {
          "type": "string",
          "pattern": "^[0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h)([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))*$",
          "description": "Go-duration string · quoted · e.g. \"30s\" \"5m\" \"1h30m\" \"2.5s\". Max 24h."
        },
        "with": {
          "type": "object",
          "description": "Task-level scope injection · `${{ with.X }}`."
        },
        "extract": {
          "type": "object",
          "description": "Named jq-expression bindings extracted from the verb's raw response · read downstream as `${{ tasks.X.<name> }}`. The field names the OPERATION (run this jq); it does not write `output` — `${{ tasks.X.output }}` stays the raw response (spec/03-dag.md §extract). jq is the single data extraction-and-transform language (the former RFC 9535 JSONPath was dropped · jq is a superset · per spec/04-variables.md §216-225). A binding may not shadow a record projection: output · status · error · started_at · ended_at · duration_ms are forbidden at parse time (spec/04-variables.md §Rules) — enforced via propertyNames.",
          "propertyNames": {
            "not": {
              "enum": [
                "output",
                "status",
                "error",
                "started_at",
                "ended_at",
                "duration_ms"
              ]
            }
          },
          "additionalProperties": {
            "type": "string",
            "format": "jq"
          }
        },
        "lift": {
          "type": "array",
          "description": "The authored doors · each entry lifts exactly ONE named law, with a mandatory reason, check-visible and receipt-recorded. A lift is NEVER a permit bypass — the value still sits inside the declared boundary, and no law but the named one moves. The law set is CLOSED and normative (a NEP amends it): this is why a new law costs zero language fields (spec/10-authority.md §the authored doors).",
          "minItems": 1,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "law",
              "because"
            ],
            "properties": {
              "law": {
                "type": "string",
                "enum": [
                  "taint",
                  "data-as-code"
                ],
                "description": "WHICH law this entry lifts · 'taint' raises ONE binding from untrusted to trusted (NEP-0004 · LAW-AUTH-0325) · 'data-as-code' declares this task's fetch a code-bearing artifact it will never load or run (NEP-0006 · LAW-AUTH-0327). Neither ever lifts the net boundary or the SSRF floor."
              },
              "from": {
                "type": "string",
                "pattern": "^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)+$",
                "description": "REQUIRED for law 'taint', forbidden otherwise · the ONE binding this entry raises (e.g. inputs.p · config.region · tasks.fetch.output) · a dotted value-binding path."
              },
              "because": {
                "type": "string",
                "minLength": 1,
                "description": "The non-empty justification · recorded in the run receipt with the taint path and the value digest."
              }
            },
            "allOf": [
              {
                "if": {
                  "properties": {
                    "law": {
                      "const": "taint"
                    }
                  }
                },
                "then": {
                  "required": [
                    "from"
                  ]
                },
                "else": {
                  "not": {
                    "required": [
                      "from"
                    ]
                  }
                }
              }
            ]
          }
        },
        "infer": {
          "$ref": "#/$defs/infer"
        },
        "exec": {
          "$ref": "#/$defs/exec"
        },
        "invoke": {
          "$ref": "#/$defs/invoke"
        },
        "agent": {
          "$ref": "#/$defs/agent"
        },
        "returns": {
          "$ref": "#/$defs/typeExpr",
          "description": "The task's output contract · the TYPED door, where a verb-level `schema:` is the out-of-core hatch. The two are exclusive on one task (NIKA-TYPE-003 · spec 09-types.md)."
        }
      }
    },
    "infer": {
      "type": "object",
      "required": [
        "prompt"
      ],
      "additionalProperties": false,
      "properties": {
        "prompt": {
          "type": "string"
        },
        "system": {
          "description": "System prompt · the standing instruction, sent ahead of `prompt:` and unchanged by it.",
          "type": "string"
        },
        "model": {
          "type": "string"
        },
        "temperature": {
          "type": [
            "number",
            "string"
          ],
          "description": "Sampling temperature · 0 is the steadiest the provider offers, 2 the loosest. A number in 0-2, or a `${{ }}` reference."
        },
        "max_tokens": {
          "type": [
            "integer",
            "string"
          ]
        },
        "schema": {
          "type": "object",
          "description": "JSON Schema · the structured-output contract the model's reply must satisfy. The out-of-core hatch; the typed door is task-level `returns:` (both on one task is NIKA-TYPE-003)."
        },
        "thinking": {
          "description": "Extended thinking · `{ enabled, budget_tokens }` — reasoning the model may spend before it answers (spec/02-verbs.md §infer).",
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "enabled": {
              "type": "boolean"
            },
            "budget_tokens": {
              "type": "integer"
            }
          }
        },
        "vision": {
          "description": "Image inputs for the call · each entry `{ source: file | url, path | url }` · the images `prompt:` is allowed to refer to.",
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "source": {
                "type": "string",
                "enum": [
                  "file",
                  "url"
                ]
              },
              "path": {
                "type": "string"
              },
              "url": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "exec": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "command": {
          "description": "argv — the program and its arguments, execve, NO shell. Each element substituted independently (the injection-safe form). Shell features (pipes · redirects · globs) live in `shell:`.",
          "type": "array",
          "items": {
            "type": "string"
          },
          "minItems": 1
        },
        "cwd": {
          "description": "Working directory for the subprocess · default = the engine's own cwd.",
          "type": "string"
        },
        "env": {
          "description": "OS environment variables for THIS subprocess · a key→value map applied over the composed environment. Nothing is inherited — the ambient environment reaches a task only through `permits.env` (spec/01-envelope.md §permits).",
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        },
        "stdin": {
          "description": "Data written to the command's standard input · may interpolate `${{ }}`.",
          "type": "string"
        },
        "capture": {
          "description": "Which stream becomes the task's output · `stdout` (default) · `stderr` · `combined` · `structured` = `{ stdout, stderr, exit_code }`. This is the SOURCE; `decode:` is how that string becomes a value.",
          "type": "string",
          "enum": [
            "stdout",
            "stderr",
            "combined",
            "structured"
          ]
        },
        "decode": {
          "type": "string",
          "enum": [
            "text",
            "json",
            "jsonl",
            "bytes"
          ],
          "description": "How the captured string becomes a value · `text` (default) · `json` · `jsonl` · `bytes`. Illegal with `capture: structured`, which already IS an object (NIKA-PARSE-025) · a non-parsing stream settles the task `failure` inside `on_error:` scope (spec 09 §decode)."
        },
        "shell": {
          "description": "One shell line, run via /bin/sh -c — the EXPLICIT dangerous door (pipes · redirects · globs). The blocklist applies here; interpolating untrusted values here is on the author. Exactly one of command|shell.",
          "type": "string",
          "minLength": 1
        }
      },
      "oneOf": [
        {
          "required": [
            "command"
          ]
        },
        {
          "required": [
            "shell"
          ]
        }
      ]
    },
    "invoke": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "tool": {
          "description": "Tool reference · nika:<path> (closed v0.1 builtin set) OR mcp:<server>/<tool> (requires the slash). The namespace set is CLOSED at v1 (spec/02-verbs.md) — an x-<vendor>: prefix is RESERVED, not valid (engine-specific tools route through mcp: · spec/06-stdlib-contract.md §Namespace ownership).",
          "oneOf": [
            {
              "type": "string",
              "description": "`nika:*` builtin · the closed canonical stdlib (Core · File · Data · Network · Introspection · Media · counts live in canon.yaml). Closed enum for `yaml-language-server` autocomplete.",
              "enum": [
                "nika:assert",
                "nika:chart",
                "nika:compose",
                "nika:convert",
                "nika:date",
                "nika:decide",
                "nika:done",
                "nika:edit",
                "nika:emit",
                "nika:fetch",
                "nika:glob",
                "nika:grep",
                "nika:hash",
                "nika:image_fx",
                "nika:image_generate",
                "nika:inspect",
                "nika:jq",
                "nika:json_diff",
                "nika:json_merge_patch",
                "nika:log",
                "nika:notify",
                "nika:prompt",
                "nika:read",
                "nika:tts_generate",
                "nika:uuid",
                "nika:validate",
                "nika:wait",
                "nika:write"
              ]
            },
            {
              "type": "string",
              "description": "`mcp:<server>/<tool>` external MCP tool · open (pattern · `/` separates path). · mcp: requires the slash (mcp:<server>/<tool> · server kebab-case · spec/02-verbs.md §tool reference grammar)",
              "pattern": "^mcp:[a-z0-9][a-z0-9-]*/[A-Za-z0-9_/-]+$"
            }
          ]
        },
        "args": {
          "description": "Arguments passed to the tool · an object whose shape is the TOOL's own schema, not the language's. Each builtin and each MCP tool declares its own (spec/02-verbs.md §invoke) · `${{ }}` may appear in any leaf value.",
          "type": "object"
        },
        "workflow": {
          "type": "string",
          "description": "Compose another workflow (spec 14) · a STATIC target: a filesystem path OR registry:owner/name@version (pinned). A templated target is refused at check (NIKA-COMP-001). Exactly one of tool: | workflow: (the invoke tagged union · G21)."
        }
      },
      "oneOf": [
        {
          "required": [
            "tool"
          ],
          "not": {
            "required": [
              "workflow"
            ]
          }
        },
        {
          "required": [
            "workflow"
          ],
          "not": {
            "required": [
              "tool"
            ]
          }
        }
      ]
    },
    "agent": {
      "type": "object",
      "required": [
        "prompt"
      ],
      "additionalProperties": false,
      "properties": {
        "prompt": {
          "type": "string"
        },
        "system": {
          "description": "System prompt · the standing instruction, sent ahead of `prompt:` and unchanged by any turn of the loop.",
          "type": "string"
        },
        "model": {
          "type": "string"
        },
        "tools": {
          "type": "array",
          "description": "Whitelist · DEFAULT-DENY (no tools if absent) · gitignore-style globs · `!` negation.",
          "items": {
            "type": "string"
          }
        },
        "skills": {
          "type": "array",
          "description": "Agent Skill (SKILL.md) file paths · explicit static paths only, no globs and no templates. They follow the agentskills.io shape, and are loaded at compose time and injected into the system context.",
          "items": {
            "type": "string"
          }
        },
        "max_turns": {
          "type": [
            "integer",
            "string"
          ]
        },
        "max_tokens_total": {
          "description": "Cumulative token budget across every turn of the loop · the SPEND ceiling, where `max_turns` is the step ceiling. Default is engine-configurable (spec/02-verbs.md §agent).",
          "type": [
            "integer",
            "string"
          ]
        },
        "temperature": {
          "type": [
            "number",
            "string"
          ],
          "description": "Sampling temperature for every turn of the loop · 0 is the steadiest the provider offers, 2 the loosest. A number in 0-2, or a `${{ }}` reference."
        },
        "schema": {
          "type": "object",
          "description": "JSON Schema · the contract the agent's FINAL message must satisfy. The out-of-core hatch; the typed door is task-level `returns:` (both on one task is NIKA-TYPE-003)."
        }
      }
    },
    "retry": {
      "type": "object",
      "required": [
        "max_attempts"
      ],
      "additionalProperties": false,
      "properties": {
        "max_attempts": {
          "description": "Total attempts, counting the first try · integer ≥ 1 · the one required field of a `retry:` block. Engines honor it strictly and surface the LAST error if every attempt fails (spec/05-errors.md §Retry policy).",
          "type": "integer",
          "minimum": 1
        },
        "backoff_ms": {
          "description": "Initial delay between attempts, in milliseconds · default 1000. `backoff_strategy` decides how it grows from there.",
          "type": "integer"
        },
        "backoff_strategy": {
          "description": "Which curve the delay follows between attempts · `fixed` · `linear` · `exponential` · default `exponential`. Each attempt waits `backoff_ms` flat, `backoff_ms × attempt`, or `backoff_ms × 2^(attempt-1)` capped at `backoff_max_ms`.",
          "type": "string",
          "enum": [
            "fixed",
            "linear",
            "exponential"
          ]
        },
        "backoff_max_ms": {
          "description": "Ceiling on the computed delay, in milliseconds · default 60000, one minute. Exponential growth stops climbing here.",
          "type": "integer"
        },
        "jitter": {
          "description": "Randomize the computed delay so tasks retrying the same upstream do not synchronize · default TRUE. Engines SHOULD use a full-jitter or equal-jitter family — the anti-thundering-herd default (spec/05-errors.md §Retry policy).",
          "type": "boolean"
        },
        "on_codes": {
          "description": "Retry ONLY on these canonical `NIKA-<NS>-<NNN>` codes · absent, the engine retries anything transient. Codes, never HTTP status numbers · the retry-side mirror of `on_error.on_codes`.",
          "type": "array",
          "items": {
            "type": "string",
            "pattern": "^NIKA-[A-Z]{2,9}(-[A-Z][A-Z0-9_]{1,15})?-[0-9]{3}$"
          }
        }
      }
    },
    "onError": {
      "type": "object",
      "additionalProperties": false,
      "description": "Error recovery · exactly ONE action (recover / skip · the oneOf enforces it) + optional on_codes filter (spec/05-errors.md §Fields · catch-side mirror of retry.on_codes · skip preserves the original error at tasks.X.error).",
      "properties": {
        "recover": {
          "description": "Recovery output · a `${{ }}` ref OR a literal (merges the former fallback/value per spec/05-errors.md)."
        },
        "skip": {
          "description": "Swallow the error and let the DAG continue · the task produces no output, and the original error stays readable at `tasks.<id>.error`. Exactly one of `recover` · `skip`.",
          "type": "boolean"
        },
        "on_codes": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "string",
            "pattern": "^NIKA-[A-Z]{2,9}(-[A-Z][A-Z0-9_]{1,15})?-[0-9]{3}$"
          },
          "description": "Optional catch-side filter (mirror of retry.on_codes · same regex) · the action applies ONLY when the final error code is listed · unlisted codes fall through to the default fail (spec/05-errors.md §Fields)."
        }
      },
      "oneOf": [
        {
          "required": [
            "recover"
          ]
        },
        {
          "required": [
            "skip"
          ]
        }
      ]
    },
    "typeExpr": {
      "description": "A Nika type expression (spec 09-types.md · closed v1 grammar · optional is FIELD-ONLY, see fieldTypeExpr)",
      "oneOf": [
        {
          "type": "null",
          "description": "the bare YAML null scalar spells the null type"
        },
        {
          "type": "string",
          "pattern": "^(null|bool|integer|number|string|bytes|uri|path|duration|timestamp)$",
          "description": "a primitive type name · named types died with the `types:` block (spec/09-types.md) so a PascalCase name resolves to nothing · money/result/artifact/secret stay reserved"
        },
        {
          "type": "object",
          "minProperties": 1,
          "maxProperties": 2,
          "properties": {
            "array": {
              "$ref": "#/$defs/typeExpr"
            },
            "map": {
              "$ref": "#/$defs/typeExpr"
            },
            "object": {
              "type": "object",
              "additionalProperties": {
                "$ref": "#/$defs/fieldTypeExpr"
              }
            },
            "additional": {
              "type": "boolean"
            },
            "union": {
              "type": "array",
              "minItems": 2,
              "items": {
                "$ref": "#/$defs/typeExpr"
              }
            },
            "enum": {
              "type": "array",
              "minItems": 1,
              "items": {
                "type": "string"
              },
              "uniqueItems": true
            },
            "integer": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "min": {
                  "type": "integer"
                },
                "max": {
                  "type": "integer"
                }
              }
            },
            "number": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "min": {
                  "type": "number"
                },
                "max": {
                  "type": "number"
                }
              }
            },
            "string": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "pattern": {
                  "type": "string",
                  "maxLength": 512
                },
                "min_len": {
                  "type": "integer",
                  "minimum": 0
                },
                "max_len": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            }
          },
          "additionalProperties": false
        }
      ]
    },
    "fieldTypeExpr": {
      "description": "A field slot (spec 09 §optional is presence): a type expression, or { optional: T } — the presence modifier is legal ONLY here",
      "oneOf": [
        {
          "$ref": "#/$defs/typeExpr"
        },
        {
          "type": "object",
          "required": [
            "optional"
          ],
          "additionalProperties": false,
          "properties": {
            "optional": {
              "$ref": "#/$defs/typeExpr"
            }
          }
        }
      ]
    }
  }
}
