{
  "$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",
    "workflow",
    "tasks"
  ],
  "additionalProperties": false,
  "properties": {
    "nika": {
      "const": "v1",
      "description": "Language contract version · exactly `v1` for the v1 lifetime. NOT `v1.0` · `1` · `1.0`."
    },
    "workflow": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9-]*$",
      "description": "Workflow id · kebab-case · unique within file · the document-type discriminator."
    },
    "description": {
      "type": "string"
    },
    "model": {
      "type": "string",
      "description": "Default model · `<provider>/<name>` (e.g. anthropic/claude-sonnet-4-6 · ollama/llama3.1 · mock/echo)."
    },
    "vars": {
      "type": "object",
      "description": "Workflow inputs · `${{ vars.X }}`. Each value is untyped (the literal default) OR a typed declaration object.",
      "additionalProperties": {
        "anyOf": [
          {
            "type": [
              "string",
              "number",
              "boolean",
              "array",
              "object",
              "null"
            ]
          },
          {
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "string",
                  "number",
                  "integer",
                  "boolean",
                  "array",
                  "object"
                ]
              },
              "required": {
                "type": "boolean"
              },
              "description": {
                "type": "string"
              },
              "default": {}
            }
          }
        ],
        "if": {
          "type": "object",
          "required": [
            "type"
          ],
          "properties": {
            "type": {
              "type": "string"
            }
          }
        },
        "then": {
          "properties": {
            "type": {
              "enum": [
                "string",
                "number",
                "integer",
                "boolean",
                "array",
                "object"
              ]
            }
          },
          "$comment": "A typed-var declaration (an object carrying a string `type:`) MUST use the closed type enum per spec/01-envelope.md §vars · the untyped-object default form (no `type:` key) is untouched."
        }
      }
    },
    "env": {
      "type": "object",
      "description": "Non-sensitive runtime config · `${{ env.X }}` · may appear in logs.",
      "additionalProperties": {
        "type": [
          "string",
          "number",
          "boolean"
        ]
      }
    },
    "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`, or a provider-egress sink `infer` / `agent` (a secret in an infer/agent prompt) · 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."
        }
      }
    },
    "tasks": {
      "type": "array",
      "minItems": 1,
      "items": {
        "$ref": "#/$defs/task"
      }
    },
    "outputs": {
      "type": "object",
      "description": "The workflow's return value · symmetric to vars. 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": {
                "type": "string",
                "enum": [
                  "string",
                  "number",
                  "integer",
                  "boolean",
                  "array",
                  "object"
                ]
              },
              "description": {
                "type": "string"
              }
            }
          }
        ]
      }
    }
  },
  "$defs": {
    "task": {
      "type": "object",
      "required": [
        "id"
      ],
      "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": {
        "id": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9_]*$",
          "description": "Task id · snake_case (CEL-safe · no hyphens) · unique within workflow."
        },
        "depends_on": {
          "type": "array",
          "items": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*$"
          }
        },
        "when": {
          "oneOf": [
            {
              "type": "boolean",
              "description": "YAML boolean literal · the always/never pattern (when: true runs regardless of upstream outcome · spec/03-dag.md §Task states)."
            },
            {
              "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": "Conditional execution gate · a ${{ }} CEL boolean OR the YAML literal true/false. An explicit when: replaces the default success-gate (spec/03-dag.md §Task states)."
        },
        "for_each": {
          "description": "Map this task over a collection · `${{ ... }}` reference OR a literal array.",
          "type": [
            "string",
            "array"
          ]
        },
        "max_parallel": {
          "type": "integer",
          "minimum": 1,
          "description": "Cap concurrent for_each iterations · default unbounded · 1 = sequential."
        },
        "fail_fast": {
          "type": "boolean",
          "description": "for_each abort-on-error · default true."
        },
        "retry": {
          "$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."
        },
        "on_finally": {
          "type": "array",
          "description": "Cleanup mini-tasks · ALWAYS run (success/fail/timeout/cancel) · sequential · best-effort.",
          "items": {
            "$ref": "#/$defs/finallyStep"
          }
        },
        "with": {
          "type": "object",
          "description": "Task-level scope injection · `${{ with.X }}`."
        },
        "output": {
          "type": "object",
          "description": "Named jq-expression bindings · `${{ tasks.X.<name> }}`. 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). Reserved names forbidden at parse time (spec/04-variables.md §Rules): output · status · error · started_at · ended_at · duration_ms — enforced via propertyNames.",
          "propertyNames": {
            "not": {
              "enum": [
                "output",
                "status",
                "error",
                "started_at",
                "ended_at",
                "duration_ms"
              ]
            }
          },
          "additionalProperties": {
            "type": "string",
            "format": "jq"
          }
        },
        "infer": {
          "$ref": "#/$defs/infer"
        },
        "exec": {
          "$ref": "#/$defs/exec"
        },
        "invoke": {
          "$ref": "#/$defs/invoke"
        },
        "agent": {
          "$ref": "#/$defs/agent"
        }
      }
    },
    "infer": {
      "type": "object",
      "required": [
        "prompt"
      ],
      "additionalProperties": false,
      "properties": {
        "prompt": {
          "type": "string"
        },
        "system": {
          "type": "string"
        },
        "model": {
          "type": "string"
        },
        "temperature": {
          "type": [
            "number",
            "string"
          ],
          "description": "0-2 · number or `${{ }}`."
        },
        "max_tokens": {
          "type": [
            "integer",
            "string"
          ]
        },
        "schema": {
          "type": "object",
          "description": "JSON Schema · structured output contract."
        },
        "thinking": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "enabled": {
              "type": "boolean"
            },
            "budget_tokens": {
              "type": "integer"
            }
          }
        },
        "vision": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "source": {
                "type": "string",
                "enum": [
                  "file",
                  "url"
                ]
              },
              "path": {
                "type": "string"
              },
              "url": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "exec": {
      "type": "object",
      "required": [
        "command"
      ],
      "additionalProperties": false,
      "properties": {
        "command": {
          "description": "String -> /bin/sh -c (shell). Array -> execve, no shell (the injection-safe form).",
          "oneOf": [
            { "type": "string" },
            {
              "type": "array",
              "items": { "type": "string" },
              "minItems": 1
            }
          ]
        },
        "cwd": {
          "type": "string"
        },
        "env": {
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        },
        "stdin": {
          "type": "string"
        },
        "capture": {
          "type": "string",
          "enum": [
            "stdout",
            "stderr",
            "combined",
            "structured"
          ]
        }
      }
    },
    "invoke": {
      "type": "object",
      "required": [
        "tool"
      ],
      "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 · 23 canonical per stdlib v0.1 (Core 6 · File 5 · Data 8 · Network 2 · Introspection 2). Closed enum for `yaml-language-server` autocomplete.",
              "enum": [
                "nika:assert",
                "nika:compose",
                "nika:convert",
                "nika:date",
                "nika:done",
                "nika:edit",
                "nika:emit",
                "nika:fetch",
                "nika:glob",
                "nika:grep",
                "nika:hash",
                "nika:inspect",
                "nika:jq",
                "nika:json_diff",
                "nika:json_merge_patch",
                "nika:log",
                "nika:notify",
                "nika:prompt",
                "nika:read",
                "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-z][a-z0-9-]*/[A-Za-z0-9_/-]+$"
            }
          ]
        },
        "args": {
          "type": "object"
        }
      }
    },
    "agent": {
      "type": "object",
      "required": [
        "prompt"
      ],
      "additionalProperties": false,
      "properties": {
        "prompt": {
          "type": "string"
        },
        "system": {
          "type": "string"
        },
        "model": {
          "type": "string"
        },
        "tools": {
          "type": "array",
          "description": "Whitelist · DEFAULT-DENY (no tools if absent) · gitignore-style globs · `!` negation.",
          "items": {
            "type": "string"
          }
        },
        "max_turns": {
          "type": [
            "integer",
            "string"
          ]
        },
        "max_tokens_total": {
          "type": [
            "integer",
            "string"
          ]
        },
        "temperature": {
          "type": [
            "number",
            "string"
          ]
        },
        "schema": {
          "type": "object",
          "description": "JSON Schema · validate the agent's final message as structured output."
        }
      }
    },
    "retry": {
      "type": "object",
      "required": [
        "max_attempts"
      ],
      "additionalProperties": false,
      "properties": {
        "max_attempts": {
          "type": "integer",
          "minimum": 1
        },
        "backoff_ms": {
          "type": "integer"
        },
        "backoff_strategy": {
          "type": "string",
          "enum": [
            "fixed",
            "linear",
            "exponential"
          ]
        },
        "backoff_max_ms": {
          "type": "integer"
        },
        "jitter": {
          "type": "boolean"
        },
        "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 / fail_workflow · 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": {
          "type": "boolean"
        },
        "fail_workflow": {
          "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"
          ]
        },
        {
          "required": [
            "fail_workflow"
          ]
        }
      ]
    },
    "finallyStep": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "when": {
          "oneOf": [
            {
              "type": "boolean",
              "description": "YAML boolean literal · the always/never pattern (when: true runs regardless of upstream outcome · spec/03-dag.md §Task states)."
            },
            {
              "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": "Conditional execution gate · a ${{ }} CEL boolean OR the YAML literal true/false. An explicit when: replaces the default success-gate (spec/03-dag.md §Task states)."
        },
        "timeout": {
          "type": "string"
        },
        "infer": {
          "$ref": "#/$defs/infer"
        },
        "exec": {
          "$ref": "#/$defs/exec"
        },
        "invoke": {
          "$ref": "#/$defs/invoke"
        },
        "agent": {
          "$ref": "#/$defs/agent"
        }
      },
      "oneOf": [
        {
          "required": [
            "infer"
          ]
        },
        {
          "required": [
            "exec"
          ]
        },
        {
          "required": [
            "invoke"
          ]
        },
        {
          "required": [
            "agent"
          ]
        }
      ]
    }
  }
}
