{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bloom.ai/schemas/workflow.schema.json",
  "title": "Bloom Stack-Agnostic Workflow Specification",
  "description": "A declarative, engine-neutral description of an SDLC workflow as a directed graph of typed nodes operating over a shared state. Any orchestration runtime (LangGraph, Temporal, XState, a hand-rolled state machine, etc.) can compile this spec down to its own primitives. Nothing in this schema references a specific library; engine-specific tuning lives only under `runtime` and `x-*` extension keys, both of which are non-normative.",
  "type": "object",
  "additionalProperties": false,
  "required": ["specVersion", "workflow", "state", "nodes", "edges", "entrypoint"],
  "properties": {
    "specVersion": {
      "type": "string",
      "description": "Version of THIS workflow-spec format (not the workflow instance). Enables migration of the format itself.",
      "pattern": "^\\d+\\.\\d+$",
      "examples": ["1.0"]
    },
    "workflow": {
      "type": "object",
      "description": "Identity and metadata for the workflow instance.",
      "additionalProperties": false,
      "required": ["id", "name", "version"],
      "properties": {
        "id": { "type": "string", "pattern": "^[a-z0-9]+(?:[-_.][a-z0-9]+)*$" },
        "name": { "type": "string" },
        "version": { "type": "string", "description": "Semantic version of this workflow definition." },
        "description": { "type": "string" },
        "owner": { "type": "string" },
        "tags": { "type": "array", "items": { "type": "string" } }
      }
    },
    "runtime": {
      "type": "object",
      "description": "NON-NORMATIVE engine hints. A compliant engine MAY read these to tune execution but MUST be able to run the workflow correctly while ignoring them entirely. Never encode required behavior here.",
      "additionalProperties": true,
      "properties": {
        "engine": {
          "type": "string",
          "description": "Preferred/reference engine this spec was authored against.",
          "examples": ["langgraph", "temporal", "xstate", "custom"]
        },
        "persistence": {
          "type": "object",
          "description": "Checkpointing / durable-state hints. The concepts (checkpoint, resume, thread) are engine-agnostic; the values are hints.",
          "additionalProperties": true,
          "properties": {
            "checkpointing": { "type": "boolean", "default": true },
            "threadKey": {
              "type": "string",
              "description": "State ref whose value identifies a durable conversation/run thread (e.g. state.chat_id).",
              "examples": ["state.thread_id"]
            }
          }
        }
      }
    },
    "integrations": {
      "type": "object",
      "description": "Registry of external systems this workflow talks to. Nodes reference integrations by key. Declaring them here keeps credentials/config out of node bodies and gives an engine one place to wire adapters.",
      "additionalProperties": { "$ref": "#/$defs/integration" }
    },
    "triggers": {
      "type": "array",
      "description": "External events that START or RESUME the workflow. A trigger is how the outside world (a Telegram message, a GitHub webhook, a cron tick) enters the graph. `event_wait` nodes reference these by id.",
      "items": { "$ref": "#/$defs/trigger" }
    },
    "state": {
      "type": "object",
      "description": "Schema of the shared state (a.k.a. context / channels / blackboard) that flows through the graph. Each field declares how concurrent writes are combined via `reducer` - this is the engine-neutral generalization of LangGraph channels, XState context, and Temporal workflow variables.",
      "additionalProperties": false,
      "required": ["fields"],
      "properties": {
        "fields": {
          "type": "object",
          "minProperties": 1,
          "additionalProperties": { "$ref": "#/$defs/stateField" }
        }
      }
    },
    "nodes": {
      "type": "array",
      "description": "The units of work. Node ids must be unique.",
      "minItems": 1,
      "items": { "$ref": "#/$defs/node" }
    },
    "edges": {
      "type": "array",
      "description": "Directed transitions between nodes. An edge with no `when` is unconditional. Multiple guarded edges out of one node express conditional routing (evaluated by descending `priority`, first match wins unless `mode` says otherwise). Multiple unconditional edges out of one node express a parallel fan-out; convergence is resolved by state-field reducers, so no explicit join syntax is required.",
      "items": { "$ref": "#/$defs/edge" }
    },
    "entrypoint": {
      "type": "string",
      "description": "Id of the node where a fresh run begins."
    },
    "terminals": {
      "type": "array",
      "description": "Ids of nodes that end a run. Optional; a node of type `terminal` or a node with no outgoing edges is also treated as an end state.",
      "items": { "type": "string" }
    }
  },
  "$defs": {
    "identifier": {
      "type": "string",
      "pattern": "^[a-zA-Z_][a-zA-Z0-9_.-]*$"
    },
    "stateRef": {
      "type": "string",
      "description": "A dotted path into shared state, always prefixed with `state.` (e.g. `state.prd.status`). Item/loop variables introduced by `map` nodes are referenced by their declared `as` name (e.g. `item.number`).",
      "pattern": "^(state|item|input|result)(\\.[a-zA-Z0-9_-]+|\\[[0-9]+\\])*$"
    },
    "stateField": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "reducer"],
      "properties": {
        "type": {
          "type": "string",
          "enum": ["string", "number", "integer", "boolean", "object", "array", "any"]
        },
        "reducer": {
          "type": "string",
          "description": "How a new write to this field combines with the existing value. `replace` = last write wins; `append` = push onto an array (list channel); `merge` = shallow-merge objects; `add` = numeric accumulation; `union` = set-union of arrays. This is the portable core of channel/reducer semantics.",
          "enum": ["replace", "append", "merge", "add", "union"]
        },
        "initial": {
          "description": "Default value at run start.",
          "$comment": "Any JSON value; intentionally unconstrained."
        },
        "description": { "type": "string" },
        "secret": {
          "type": "boolean",
          "default": false,
          "description": "If true, the engine should redact this field from logs/checkpoints where possible."
        }
      }
    },
    "integration": {
      "type": "object",
      "additionalProperties": false,
      "required": ["kind"],
      "properties": {
        "kind": {
          "type": "string",
          "description": "The category of system, not a concrete SDK. Adapters bind these to real clients.",
          "examples": ["messaging", "vcs", "llm", "ci", "deploy", "http", "storage"]
        },
        "provider": {
          "type": "string",
          "description": "Concrete provider, swappable without touching node logic.",
          "examples": ["telegram", "github", "gitlab", "anthropic", "openai", "openclaw"]
        },
        "config": {
          "type": "object",
          "description": "Non-secret configuration. Secrets are injected by the runtime by convention (e.g. ${env:...}), never inlined.",
          "additionalProperties": true
        },
        "actions": {
          "type": "array",
          "description": "Optional allow-list of action names a `tool` node may invoke on this integration. Documents the contract the adapter must satisfy.",
          "items": { "type": "string" }
        }
      }
    },
    "trigger": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "type"],
      "properties": {
        "id": { "$ref": "#/$defs/identifier" },
        "type": {
          "type": "string",
          "enum": ["message", "webhook", "schedule", "manual"],
          "description": "message = inbound chat; webhook = external system callback; schedule = time-based; manual = operator-invoked."
        },
        "integration": { "type": "string", "description": "Key into `integrations`." },
        "filter": {
          "type": "object",
          "description": "Optional constraints that must hold for the event to fire this trigger (e.g. webhook event == pull_request, action in [opened, synchronize]).",
          "additionalProperties": true
        },
        "binds": {
          "type": "object",
          "description": "Map of state field <- path in the event payload, describing how the inbound event hydrates state.",
          "additionalProperties": { "type": "string" }
        },
        "description": { "type": "string" }
      }
    },
    "node": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "type"],
      "properties": {
        "id": { "$ref": "#/$defs/identifier" },
        "type": {
          "type": "string",
          "description": "The engine-neutral node kinds. Every kind maps cleanly onto a node/state/activity in mainstream engines.",
          "enum": [
            "agent",
            "tool",
            "human",
            "router",
            "map",
            "subworkflow",
            "event_wait",
            "passthrough",
            "terminal"
          ]
        },
        "title": { "type": "string" },
        "description": { "type": "string" },
        "phase": {
          "type": "string",
          "description": "SDLC phase this node belongs to, for grouping/observability.",
          "examples": ["inception", "planning", "execution", "review", "deployment", "testing", "feedback"]
        },
        "mvp": {
          "type": "boolean",
          "default": true,
          "description": "false marks a node that expresses a post-MVP (future) capability, so the same spec documents the full lifecycle while an engine can prune non-MVP nodes."
        },
        "reads": {
          "type": "array",
          "description": "State refs this node consumes. Advisory; enables static wiring/validation.",
          "items": { "$ref": "#/$defs/stateRef" }
        },
        "writes": {
          "type": "array",
          "description": "State fields this node produces. Advisory; must be consistent with `state.fields`.",
          "items": { "type": "string" }
        },
        "retry": { "$ref": "#/$defs/retryPolicy" },
        "config": {
          "type": "object",
          "description": "Type-specific configuration. Its shape is selected by the node's `type` (see the allOf discriminator below). For `router`, `passthrough`, and `terminal` nodes it is optional and free-form.",
          "additionalProperties": true
        }
      },
      "allOf": [
        {
          "if": { "properties": { "type": { "const": "agent" } } },
          "then": { "required": ["config"], "properties": { "config": { "$ref": "#/$defs/agentConfig" } } }
        },
        {
          "if": { "properties": { "type": { "const": "tool" } } },
          "then": { "required": ["config"], "properties": { "config": { "$ref": "#/$defs/toolConfig" } } }
        },
        {
          "if": { "properties": { "type": { "const": "human" } } },
          "then": { "required": ["config"], "properties": { "config": { "$ref": "#/$defs/humanConfig" } } }
        },
        {
          "if": { "properties": { "type": { "const": "map" } } },
          "then": { "required": ["config"], "properties": { "config": { "$ref": "#/$defs/mapConfig" } } }
        },
        {
          "if": { "properties": { "type": { "const": "subworkflow" } } },
          "then": { "required": ["config"], "properties": { "config": { "$ref": "#/$defs/subworkflowConfig" } } }
        },
        {
          "if": { "properties": { "type": { "const": "event_wait" } } },
          "then": { "required": ["config"], "properties": { "config": { "$ref": "#/$defs/eventWaitConfig" } } }
        }
      ]
    },
    "agentConfig": {
      "type": "object",
      "description": "An LLM-driven step. The prompt/model/tooling are described abstractly; the concrete LLM is bound via an `llm` integration so the provider is swappable.",
      "additionalProperties": false,
      "required": ["agent"],
      "properties": {
        "agent": {
          "type": "string",
          "description": "Logical agent/role id (e.g. product_owner, reviewer). Lets several nodes share one persona definition."
        },
        "model": { "type": "string", "description": "Key into `integrations` of kind llm. Provider-neutral." },
        "instructions": {
          "type": "string",
          "description": "System/role instructions, or a ${ref:...} to an external prompt asset."
        },
        "input": {
          "type": "object",
          "description": "Map of prompt-variable <- state ref. Decouples prompt templating from state layout.",
          "additionalProperties": { "$ref": "#/$defs/stateRef" }
        },
        "tools": {
          "type": "array",
          "description": "Tool/integration action ids the agent may call during this step.",
          "items": { "type": "string" }
        },
        "output": { "$ref": "#/$defs/outputSpec" }
      }
    },
    "toolConfig": {
      "type": "object",
      "description": "A deterministic call to an integration action (no LLM). This is where side effects to external systems live.",
      "additionalProperties": false,
      "required": ["tool", "integration", "action"],
      "properties": {
        "tool": { "type": "string", "description": "Human-readable id for this operation." },
        "integration": { "type": "string", "description": "Key into `integrations`." },
        "action": { "type": "string", "description": "Action name on that integration (must be in its `actions` allow-list if declared)." },
        "params": {
          "type": "object",
          "description": "Map of action-parameter <- state ref or literal.",
          "additionalProperties": true
        },
        "output": { "$ref": "#/$defs/outputSpec" }
      }
    },
    "humanConfig": {
      "type": "object",
      "description": "A human-in-the-loop gate. Execution PAUSES here (the engine-neutral notion of an interrupt) until an external response resumes it. Maps to LangGraph interrupts, Temporal signals, XState external events.",
      "additionalProperties": false,
      "required": ["human", "channel"],
      "properties": {
        "human": { "type": "string", "description": "Id of the decision being requested (e.g. prd_approval)." },
        "channel": { "type": "string", "description": "Integration used to ask the human (e.g. telegram)." },
        "prompt": { "type": "string", "description": "What to ask, or a ${ref:...} to a template." },
        "input": {
          "type": "object",
          "additionalProperties": { "$ref": "#/$defs/stateRef" }
        },
        "awaits": {
          "type": "object",
          "description": "Schema of the expected human response; its parsed value is written per `output`.",
          "additionalProperties": true
        },
        "timeout": { "$ref": "#/$defs/duration" },
        "output": { "$ref": "#/$defs/outputSpec" }
      }
    },
    "mapConfig": {
      "type": "object",
      "description": "Fan-out over a collection: run `body` once per element, then collect. Portable generalization of LangGraph's Send / parallel branches.",
      "additionalProperties": false,
      "required": ["map", "over", "as", "body"],
      "properties": {
        "map": { "type": "string" },
        "over": { "$ref": "#/$defs/stateRef", "description": "State ref to the array being iterated." },
        "as": { "$ref": "#/$defs/identifier", "description": "Loop-variable name each body invocation binds (referenced as `item.*`)." },
        "body": {
          "type": "string",
          "description": "Node id or subworkflow id executed per element."
        },
        "concurrency": {
          "type": "integer",
          "minimum": 1,
          "description": "Optional max parallelism hint."
        },
        "collect": {
          "type": "string",
          "description": "State field the per-item results are reduced into (field's reducer decides how)."
        }
      }
    },
    "subworkflowConfig": {
      "type": "object",
      "additionalProperties": false,
      "required": ["subworkflow", "ref"],
      "properties": {
        "subworkflow": { "type": "string" },
        "ref": { "type": "string", "description": "workflow.id (or path) of the nested spec to invoke." },
        "input": { "type": "object", "additionalProperties": { "$ref": "#/$defs/stateRef" } },
        "output": { "$ref": "#/$defs/outputSpec" }
      }
    },
    "eventWaitConfig": {
      "type": "object",
      "description": "Suspend until a declared trigger fires (or timeout). This is how long-lived, event-driven SDLC flows park between human/system activity without holding resources.",
      "additionalProperties": false,
      "required": ["eventWait", "trigger"],
      "properties": {
        "eventWait": { "type": "string" },
        "trigger": { "type": "string", "description": "Id of the trigger this node waits for." },
        "timeout": { "$ref": "#/$defs/duration" }
      }
    },
    "outputSpec": {
      "type": "object",
      "description": "Where and how a node's result lands in shared state.",
      "additionalProperties": false,
      "properties": {
        "assign": {
          "type": "object",
          "description": "Map of state field <- path in this node's result (e.g. { \"prd\": \"result\", \"prd.status\": \"result.status\" }).",
          "additionalProperties": { "type": "string" }
        },
        "schema": {
          "type": "object",
          "description": "Optional JSON Schema the result must satisfy (enables structured-output enforcement).",
          "additionalProperties": true
        }
      }
    },
    "edge": {
      "type": "object",
      "additionalProperties": false,
      "required": ["from", "to"],
      "properties": {
        "id": { "type": "string" },
        "from": { "type": "string", "description": "Source node id." },
        "to": { "type": "string", "description": "Target node id (or a terminal node id)." },
        "when": {
          "$ref": "#/$defs/expression",
          "description": "Guard. Omit for an unconditional transition."
        },
        "priority": {
          "type": "integer",
          "default": 0,
          "description": "Higher evaluates first among guarded edges from the same node."
        },
        "label": { "type": "string" }
      }
    },
    "expression": {
      "description": "A declarative, language-neutral predicate over state. Avoids embedding host code so the same guard runs under any engine. Use `predicate` only as an escape hatch to a host-registered named function.",
      "oneOf": [
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["op", "left"],
          "properties": {
            "op": { "type": "string", "enum": ["eq", "ne", "lt", "lte", "gt", "gte", "in", "contains", "exists", "truthy", "matches"] },
            "left": { "$ref": "#/$defs/operand" },
            "right": { "$ref": "#/$defs/operand" }
          }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["all"],
          "properties": { "all": { "type": "array", "items": { "$ref": "#/$defs/expression" } } }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["any"],
          "properties": { "any": { "type": "array", "items": { "$ref": "#/$defs/expression" } } }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["not"],
          "properties": { "not": { "$ref": "#/$defs/expression" } }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["predicate"],
          "properties": {
            "predicate": { "type": "string", "description": "Name of a host-registered predicate. Escape hatch; prefer structured ops." },
            "args": { "type": "object", "additionalProperties": true }
          }
        }
      ]
    },
    "operand": {
      "description": "Either a reference into state or a literal constant.",
      "oneOf": [
        { "type": "object", "additionalProperties": false, "required": ["ref"], "properties": { "ref": { "$ref": "#/$defs/stateRef" } } },
        { "type": "object", "additionalProperties": false, "required": ["const"], "properties": { "const": {} } }
      ]
    },
    "retryPolicy": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "maxAttempts": { "type": "integer", "minimum": 1, "default": 1 },
        "backoff": { "type": "string", "enum": ["none", "fixed", "exponential"], "default": "exponential" },
        "initialDelay": { "$ref": "#/$defs/duration" }
      }
    },
    "duration": {
      "type": "string",
      "description": "ISO-8601 duration (e.g. PT30S, PT10M, P1D).",
      "pattern": "^P(?:\\d+Y)?(?:\\d+M)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+S)?)?$"
    }
  }
}
