{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://itlackey.github.io/akm/schemas/akm-workflow.json",
  "title": "AKM Workflow",
  "description": "This frontmatter schema applies to and validates the Markdown (.md) workflow source: the standard AKM asset envelope plus its orchestration graph. Markdown is not the sole or only workflow source; peer GitHub-shaped .yml sources use the bounded adapter and both formats compile to source IR v1. Per-step prose instructions and gate rubrics live in the Markdown body, joined to this graph by step id. Hand-authored alongside src/workflows/program/schema.ts; see docs/reference/workflow-schema.md.",
  "type": "object",
  "required": ["steps"],
  "additionalProperties": false,
  "properties": {
    "type": { "$ref": "akm-asset-envelope.json#/definitions/type" },
    "description": { "$ref": "akm-asset-envelope.json#/definitions/description" },
    "tags": { "$ref": "akm-asset-envelope.json#/definitions/tags" },
    "when_to_use": { "$ref": "akm-asset-envelope.json#/definitions/when_to_use" },
    "xrefs": { "$ref": "akm-asset-envelope.json#/definitions/xrefs" },
    "updated": { "$ref": "akm-asset-envelope.json#/definitions/updated" },
    "timestamp": { "$ref": "akm-asset-envelope.json#/definitions/timestamp" },
    "generated": { "$ref": "akm-asset-envelope.json#/definitions/generated" },
    "verified": { "$ref": "akm-asset-envelope.json#/definitions/verified" },
    "provenance": { "$ref": "akm-asset-envelope.json#/definitions/provenance" },
    "status": { "$ref": "akm-asset-envelope.json#/definitions/status" },
    "stale_after": { "$ref": "akm-asset-envelope.json#/definitions/stale_after" },
    "params": {
      "type": "object",
      "minProperties": 1,
      "description": "Run parameters: param name -> JSON Schema declaration. Names must be params.<name>-addressable identifiers.",
      "propertyNames": {
        "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
      },
      "additionalProperties": {
        "$ref": "#/definitions/jsonSchemaObject"
      }
    },
    "outputs": {
      "type": "object",
      "minProperties": 1,
      "description": "Named, optionally schema-validated projections of step artifacts, exported when the run completes. Names must be steps.<child>.output.<name>-addressable identifiers. Each entry: { from: steps.<id>.output(.<seg>)*, schema?: <bounded JSON Schema> }. Markdown-frontmatter only.",
      "propertyNames": {
        "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
      },
      "additionalProperties": {
        "$ref": "#/definitions/outputDeclaration"
      }
    },
    "defaults": {
      "$ref": "#/definitions/defaults"
    },
    "budget": {
      "$ref": "#/definitions/budget"
    },
    "steps": {
      "type": "array",
      "minItems": 1,
      "items": {
        "$ref": "#/definitions/step"
      }
    }
  },
  "definitions": {
    "identifier": {
      "type": "string",
      "pattern": "^[A-Za-z_][A-Za-z0-9_-]*$",
      "description": "Step id: a letter or underscore first, then letters, digits, underscores, or dashes (no dots, no leading digit). This is exactly the steps.<id>.output-addressable grammar; dots are reserved for the .output path separator and the engine's internal <stepId>.gate node id."
    },
    "reference": {
      "type": "string",
      "minLength": 1,
      "description": "A bare reference string: params.<name> or steps.<id>.output(.<ident>|[<int>])*. No ${{ }} delimiters — every position this appears in is a whole-value frontmatter field. Full grammar validation happens at compile time, not here."
    },
    "timeout": {
      "description": "Duration: \"<n>ms\" | \"<n>s\" | \"<n>m\" | \"none\" (bare integers are milliseconds). Must resolve to at most 2147483647 ms (setTimeout's 32-bit signed ceiling) — the parser enforces the same ceiling on the string form.",
      "oneOf": [
        {
          "type": "string",
          "pattern": "^([0-9]+(ms|s|m)?|none)$"
        },
        {
          "type": "integer",
          "minimum": 1,
          "maximum": 2147483647
        }
      ]
    },
    "engineName": {
      "type": "string",
      "minLength": 1,
      "maxLength": 63,
      "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
      "description": "Named engine: lowercase dash-separated words of letters and digits, starting with a letter. Same grammar the frozen-plan decoder enforces (src/workflows/resource-limits.ts WORKFLOW_ENGINE_NAME_PATTERN)."
    },
    "onError": {
      "description": "Failure policy: fail the step on the first unit failure (default), or record failures and let the gate decide.",
      "enum": ["fail", "continue"]
    },
    "llm": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "temperature": { "type": "number" },
        "max_tokens": { "type": "integer", "minimum": 1 },
        "supports_json_schema": { "type": "boolean" },
        "extra_params": { "$ref": "#/definitions/extraParams" },
        "context_length": { "type": "integer", "minimum": 1 },
        "enable_thinking": { "type": "boolean" },
        "reasoning_effort": { "type": "string", "minLength": 1 }
      }
    },
    "extraParams": {
      "type": "object",
      "description": "Provider-specific request fields. Top-level AKM-owned fields are forbidden; credential-shaped keys are forbidden recursively after lowercase/non-alphanumeric normalization.",
      "propertyNames": {
        "not": {
          "enum": ["model", "messages", "temperature", "maxtokens", "responseformat", "stream", "streamoptions", "enablethinking", "reasoningeffort", "chattemplatekwargs", "authorization", "headers", "apikey", "token", "password", "secret", "cookie", "setcookie"]
        }
      },
      "additionalProperties": { "$ref": "#/definitions/extraParamValue" },
      "x-akm-protectedTopLevelNormalizedKeys": ["model", "messages", "temperature", "maxtokens", "responseformat", "stream", "streamoptions", "enablethinking", "reasoningeffort", "chattemplatekwargs"],
      "x-akm-recursivelyForbiddenNormalizedKeys": ["authorization", "headers", "apikey", "token", "password", "secret", "cookie", "setcookie"]
    },
    "extraParamValue": {
      "anyOf": [
        { "type": ["string", "number", "boolean", "null"] },
        { "type": "array", "items": { "$ref": "#/definitions/extraParamValue" } },
        {
          "type": "object",
          "propertyNames": {
            "not": {
              "enum": ["authorization", "headers", "apikey", "token", "password", "secret", "cookie", "setcookie"]
            }
          },
          "additionalProperties": { "$ref": "#/definitions/extraParamValue" },
          "x-akm-recursivelyForbiddenNormalizedKeys": ["authorization", "headers", "apikey", "token", "password", "secret", "cookie", "setcookie"]
        }
      ]
    },
    "reducer": {
      "description": "How a map step folds per-item unit results into the step artifact.",
      "enum": ["collect", "vote"]
    },
    "isolation": {
      "description": "Filesystem isolation for file-mutating units.",
      "enum": ["none", "worktree"]
    },
    "failureReason": {
      "description": "Persisted AgentFailureReason taxonomy (src/integrations/agent/spawn.ts).",
      "enum": [
        "timeout",
        "spawn_failed",
        "non_zero_exit",
        "parse_error",
        "cooldown",
        "llm_rate_limit",
        "llm_content_filter",
        "llm_invalid_json",
        "content_policy_reject",
        "unsupported_type",
        "no_change",
        "aborted"
      ]
    },
    "jsonSchemaObject": {
      "type": "object",
      "description": "A JSON Schema declaration. The parser validates it as a schema definition against the runtime's enforced subset (src/core/json-schema.ts checkJsonSchemaDefinition): typo'd type names and keywords outside the subset (e.g. $ref, const, format, patternProperties) are authoring-time errors, not silent no-ops."
    },
    "outputDeclaration": {
      "type": "object",
      "description": "One outputs: entry: a validated steps.<id>.output(.<seg>)* reference into a step artifact, plus an optional bounded JSON Schema.",
      "required": ["from"],
      "additionalProperties": false,
      "properties": {
        "from": {
          "type": "string",
          "description": "A steps.<id>.output(.<seg>)* reference into a step artifact. Never params.<name> — an output projects a step artifact, never a param."
        },
        "schema": {
          "$ref": "#/definitions/jsonSchemaObject"
        }
      }
    },
    "retry": {
      "type": "object",
      "description": "Bounded retry on transient failures, keyed on the persisted failure_reason taxonomy.",
      "required": ["max", "on"],
      "additionalProperties": false,
      "properties": {
        "max": {
          "type": "integer",
          "minimum": 0,
          "description": "Maximum retry attempts per unit."
        },
        "on": {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/definitions/failureReason"
          }
        }
      }
    },
    "budget": {
      "type": "object",
      "description": "Run-level budget ceilings, enforced by the engine per run (journal-seeded). Hitting a ceiling fails the step hard, regardless of on_error.",
      "additionalProperties": false,
      "properties": {
        "max_tokens": {
          "type": "integer",
          "minimum": 1,
          "description": "Ceiling on total reported token usage across the run (seeded from journaled unit rows)."
        },
        "max_units": {
          "type": "integer",
          "minimum": 1,
          "description": "Ceiling on total dispatched units across the run (seeded from journaled unit rows)."
        }
      }
    },
    "defaults": {
      "type": "object",
      "description": "Run-level defaults, overridable per unit.",
      "additionalProperties": false,
      "properties": {
        "engine": {
          "$ref": "#/definitions/engineName"
        },
        "model": {
          "type": "string",
          "minLength": 1,
          "description": "Model alias (tier) or exact id; resolved per-harness at dispatch."
        },
        "timeout": {
          "$ref": "#/definitions/timeout"
        },
        "on_error": {
          "$ref": "#/definitions/onError"
        },
        "llm": {
          "$ref": "#/definitions/llm"
        }
      }
    },
    "exec": {
      "type": "object",
      "description": "Run a shell command as this unit instead of dispatching to an engine. `command` is an ARGV ARRAY and there is no shell-string form: the child is spawned directly, so shell metacharacters (`;` `|` `&&` `$(…)` `>` `*`) are inert literal argument bytes — the quoting/injection class simply does not exist. Write the interpreter explicitly (e.g. [\"bash\", \"-lc\", \"a | b\"]) when a pipeline is really wanted. stdout is the promoted step artifact with trailing newlines stripped (like shell `$(…)`); when the unit declares an `output` schema, stdout must instead be exactly one JSON value, which is validated against it. A non-zero exit fails the unit with failure reason `non_zero_exit`, honoring `retry:` and `on_error:` like any other unit. The child's environment is an ALLOWLIST by default (PATH, HOME, locale/temp vars, the Windows process-creation essentials) — widen it only with named `env:` environment bindings or `pass_env`.",
      "required": ["command"],
      "additionalProperties": false,
      "properties": {
        "command": {
          "type": "array",
          "description": "argv; command[0] is the program, resolved through PATH. Never shell-parsed.",
          "minItems": 1,
          "items": {
            "type": "string",
            "minLength": 1
          }
        },
        "cwd": {
          "type": "string",
          "description": "Optional RELATIVE working directory inside the unit's working directory (its fresh worktree under `isolation: worktree`). Absolute paths, Windows drive letters, \"~\", and \"..\" segments are rejected here, and containment is re-checked against the resolved base — symlinks included — before the command is spawned.",
          "minLength": 1,
          "pattern": "^(?!/|\\\\|~|[A-Za-z]:)(?!.*(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$)).+$"
        },
        "pass_env": {
          "type": "array",
          "description": "Extra environment variable NAMES copied through from akm's own environment on top of the default allowlist — for a per-machine toolchain variable (e.g. CARGO_HOME) that no committed `env:` asset could carry. NAMES ONLY: values never appear in a workflow, and unlike `env:` bindings these values are NOT redacted from the command's output, so never list a credential here.",
          "minItems": 1,
          "uniqueItems": true,
          "items": {
            "type": "string",
            "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
          }
        }
      }
    },
    "unit": {
      "type": "object",
      "description": "Optional dispatch-override bag for a unit/map step. A step with none of these carries the run's engine/model/timeout defaults verbatim. Declaring `exec` turns the step into a shell-command unit: it names no engine, so `engine`, `model`, and `llm` are rejected alongside it.",
      "additionalProperties": false,
      "allOf": [
        {
          "if": {
            "required": ["exec"]
          },
          "then": {
            "not": {
              "anyOf": [
                {
                  "required": ["engine"]
                },
                {
                  "required": ["model"]
                },
                {
                  "required": ["llm"]
                }
              ]
            }
          }
        }
      ],
      "properties": {
        "exec": {
          "$ref": "#/definitions/exec"
        },
        "engine": {
          "$ref": "#/definitions/engineName",
          "description": "Named engine selected when the workflow run is created."
        },
        "model": {
          "type": "string",
          "minLength": 1
        },
        "llm": {
          "$ref": "#/definitions/llm"
        },
        "timeout": {
          "$ref": "#/definitions/timeout"
        },
        "retry": {
          "$ref": "#/definitions/retry"
        },
        "on_error": {
          "$ref": "#/definitions/onError"
        },
        "output": {
          "$ref": "#/definitions/jsonSchemaObject"
        },
        "env": {
          "type": "array",
          "uniqueItems": true,
          "items": {
            "type": "string",
            "minLength": 1
          },
          "description": "Env asset refs injected into the dispatched unit env. Entries must be unique: the frozen-plan decoder rejects duplicates at run start."
        },
        "isolation": {
          "$ref": "#/definitions/isolation"
        }
      }
    },
    "map": {
      "type": "object",
      "description": "Fan the step out over a reference-addressed item list.",
      "required": ["over"],
      "additionalProperties": false,
      "properties": {
        "over": {
          "$ref": "#/definitions/reference",
          "description": "Reference naming the producer of the item list (e.g. steps.discover.output.files)."
        },
        "concurrency": {
          "type": "integer",
          "minimum": 1,
          "maximum": 64,
          "description": "Max concurrent units for this step. Defaults to workflow.defaultMapConcurrency (4 when unset) — a map step is parallel by default since 0.9.1; write 1 for serial execution. The effective width is the minimum of this, workflow.maxConcurrency, the selected engine's concurrency, and the host CPU cap."
        },
        "reducer": {
          "$ref": "#/definitions/reducer"
        },
        "unit": {
          "$ref": "#/definitions/unit"
        }
      }
    },
    "routeBranch": {
      "type": "object",
      "required": ["match", "step"],
      "additionalProperties": false,
      "properties": {
        "match": {
          "type": ["string", "number", "boolean"],
          "minLength": 1,
          "description": "Branch key. An empty string is rejected: the frozen-plan decoder requires every `when` key to be non-empty."
        },
        "step": {
          "$ref": "#/definitions/identifier"
        }
      }
    },
    "route": {
      "type": "object",
      "description": "Route on an explicit reference input to a LATER step. Targets must come after the routing step.",
      "required": ["input", "when"],
      "additionalProperties": false,
      "properties": {
        "input": {
          "$ref": "#/definitions/reference",
          "description": "Reference naming the value to route on."
        },
        "when": {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/definitions/routeBranch"
          }
        },
        "default": {
          "$ref": "#/definitions/identifier"
        }
      }
    },
    "gate": {
      "type": "object",
      "description": "Optional validation-loop configuration. The rubric itself lives in the body under a `### gate` sub-heading inside the step's section; omitted or empty rubric text skips validation.",
      "additionalProperties": false,
      "properties": {
        "max_loops": {
          "type": "integer",
          "minimum": 1,
          "description": "Evaluator-optimizer loop bound."
        }
      }
    },
    "inputs": {
      "type": "array",
      "minItems": 1,
      "uniqueItems": true,
      "items": {
        "$ref": "#/definitions/reference"
      },
      "description": "Prior-step artifacts this unit/map step consumes, as reference strings (sub-paths legal). Attached to the dispatched unit as structured context; never spliced into instructions. Entries must be unique: the frozen-plan decoder rejects duplicates at run start."
    },
    "step": {
      "type": "object",
      "description": "One step of the gated spine. A step with no map/route is a unit step (bare `{id: ...}` is the complete minimal declaration). At most one of map/route.",
      "required": ["id"],
      "additionalProperties": false,
      "properties": {
        "id": {
          "$ref": "#/definitions/identifier"
        },
        "unit": {
          "$ref": "#/definitions/unit"
        },
        "map": {
          "$ref": "#/definitions/map"
        },
        "route": {
          "$ref": "#/definitions/route"
        },
        "inputs": {
          "$ref": "#/definitions/inputs"
        },
        "output": {
          "$ref": "#/definitions/jsonSchemaObject"
        },
        "gate": {
          "$ref": "#/definitions/gate"
        }
      },
      "allOf": [
        {
          "not": { "required": ["map", "route"] }
        },
        {
          "if": { "required": ["route"] },
          "then": { "not": { "anyOf": [{ "required": ["unit"] }, { "required": ["map"] }, { "required": ["inputs"] }] } }
        },
        {
          "if": { "required": ["map"] },
          "then": { "not": { "required": ["unit"] } }
        }
      ]
    }
  }
}
