openapi: 3.0.3
info:
  title: Orchestrations API
  version: 1.0.0
  description: >
    Declarative workflow execution layer for SOAT. Define multi-step pipelines as
    directed graphs, execute them as runs, and inspect state and artifacts.
  contact:
    name: SOAT API Support

servers:
  - url: '{baseUrl}'
    description: Base URL of your SOAT deployment (e.g. https://your-soat.com or http://localhost:5047)
    variables:
      baseUrl:
        description: The base URL of your SOAT deployment
        default: http://localhost:5047

tags:
  - name: Orchestrations
    description: Manage orchestrations and their runs

security:
  - bearerAuth: []

paths:
  /api/v1/orchestrations:
    post:
      tags:
        - Orchestrations
      summary: Create an orchestration
      description: Creates a new orchestration (pipeline) definition in the project.
      operationId: createOrchestration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrchestrationRequest'
      responses:
        '201':
          description: Orchestration created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Orchestration'
        '400':
          description: Validation error
        '401':
          description: Unauthorized
        '403':
          description: Forbidden

    get:
      tags:
        - Orchestrations
      summary: List orchestrations
      description: Returns orchestrations accessible to the caller.
      operationId: listOrchestrations
      parameters:
        - in: query
          name: project_id
          schema:
            type: string
          description: Filter by project public ID
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of orchestrations
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Orchestration'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden

  /api/v1/orchestrations/validate:
    post:
      tags:
        - Orchestrations
      summary: Validate an orchestration graph
      description: >
        Statically validates an orchestration graph without persisting
        anything. Checks that every node has its required field, node ids are
        unique, edges reference existing nodes, the graph is acyclic (unless it
        contains a loop node), and every `input_mapping` `{"var": "..."}`
        reference resolves to a state key written by an upstream node or seeded
        by `input_schema`. Returns blocking `errors` and non-blocking
        `warnings` (e.g. a state key only written on a conditional branch). The
        same `errors` checks are enforced on create and update, which fail with
        `400` when any error is present.
      operationId: validateOrchestration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateOrchestrationRequest'
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationResult'
        '401':
          description: Unauthorized

  /api/v1/orchestrations/queue/stats:
    get:
      tags:
        - Orchestrations
      summary: Get orchestration queue stats
      description: >
        Returns a point-in-time snapshot of the orchestration run queue: how
        many tasks are waiting to be claimed (`queue_depth`), how many are
        currently claimed with a valid lease (`claimed_tasks`), the age of the
        oldest waiting task, recent claim-latency percentiles over a rolling
        in-process window, and a per-project breakdown. Intended for
        admin/operator policies; guarded by `orchestrations:GetQueueStats`. A
        project-scoped caller sees only their own projects under `per_project`.
      operationId: getQueueStats
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Queue stats snapshot
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueueStats'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden

  /api/v1/orchestrations/{orchestration_id}:
    get:
      tags:
        - Orchestrations
      summary: Get an orchestration
      description: Returns the orchestration with nodes and edges.
      operationId: getOrchestration
      parameters:
        - $ref: '#/components/parameters/orchestration_id'
      responses:
        '200':
          description: Orchestration details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Orchestration'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found

    patch:
      tags:
        - Orchestrations
      summary: Update an orchestration
      description: Partially updates an orchestration definition.
      operationId: updateOrchestration
      parameters:
        - $ref: '#/components/parameters/orchestration_id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateOrchestrationRequest'
      responses:
        '200':
          description: Updated orchestration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Orchestration'
        '400':
          description: Validation error
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found

    delete:
      tags:
        - Orchestrations
      summary: Delete an orchestration
      description: Deletes an orchestration definition and all its runs.
      operationId: deleteOrchestration
      parameters:
        - $ref: '#/components/parameters/orchestration_id'
      responses:
        '204':
          description: Deleted
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found

  /api/v1/orchestrations/{orchestration_id}/versions:
    get:
      tags:
        - Orchestrations
      summary: List an orchestration's graph versions
      description: >
        Returns the orchestration's archived graphs, newest first. A version is
        written on create and on every subsequent write that changes the graph
        (`nodes`, `edges`, `state_schema`, `input_schema`) — through the REST API
        or a formation apply alike. Metadata-only edits (name, description) do not
        archive a version. See
        [Versioning](/docs/modules/orchestrations#versioning).
      operationId: listOrchestrationVersions
      parameters:
        - $ref: '#/components/parameters/orchestration_id'
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of orchestration versions, newest first
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/OrchestrationVersion'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Orchestration not found

  /api/v1/orchestrations/{orchestration_id}/versions/{version}:
    get:
      tags:
        - Orchestrations
      summary: Fetch an archived orchestration version
      description: >
        Returns the exact graph a given version describes. Every run records the
        version it started on in `orchestration_version` and executes that graph
        for its whole life, so this is how you read the topology a run actually
        took — including a run whose orchestration has been rewired since.
      operationId: getOrchestrationVersion
      parameters:
        - $ref: '#/components/parameters/orchestration_id'
        - name: version
          in: path
          required: true
          description: The archived version number
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Archived orchestration version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrationVersion'
        '400':
          description: Bad Request — version is not a positive integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found

  /api/v1/orchestrations/{orchestration_id}/versions/{version}/restore:
    post:
      tags:
        - Orchestrations
      summary: Restore an archived orchestration graph
      description: >
        Writes an archived version's graph back as the orchestration's live
        definition, which archives it again as a **new** version rather than
        rewinding the counter — so a run pinned to any version in between still
        resolves the graph it started on.


        The restore runs through the ordinary update path, so the archived graph
        goes through the same static validation as an authored one. Node resource
        references (`agent_id`, `tool_id`, `orchestration_id`) resolve when a run
        reaches the node, so a target deleted since the snapshot was taken restores
        cleanly and surfaces as a failed run rather than a `400`. Restoring the
        graph the orchestration already holds is a no-op and archives nothing. Runs
        already in flight are unaffected either way — a restore is an ordinary
        edit, and pinning is what keeps it from reaching them.
      operationId: restoreOrchestrationVersion
      parameters:
        - $ref: '#/components/parameters/orchestration_id'
        - name: version
          in: path
          required: true
          description: The archived version number
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RestoreOrchestrationVersionRequest'
      responses:
        '200':
          description: The orchestration, at its new version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Orchestration'
        '400':
          description: Bad Request — version is not a positive integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found

  /api/v1/orchestration-runs:
    post:
      tags:
        - Orchestrations
      summary: Start an orchestration run
      description: >-
        Creates a new run for the orchestration named by orchestration_id. By
        default the run executes durably in the background: the response returns
        immediately with status "queued" (a worker then claims it and moves it
        to "running") and progress is observed via
        get-orchestration-run or run lifecycle webhook events
        (orchestration_runs.started/awaiting_input/succeeded/failed). Delay and
        poll waits park the run as "sleeping" and are woken by a background
        scheduler, surviving restarts. Pass wait=true to block until the run
        reaches a terminal or awaiting_input state.
      operationId: startOrchestrationRun
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StartRunRequest'
      responses:
        '201':
          description: Run created and executed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrationRun'
        '400':
          description: >-
            Validation error (e.g. a `tool_context` key that cannot become a
            header, or `metadata` that is not a JSON object). No run is created.
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Orchestration not found

    get:
      tags:
        - Orchestrations
      summary: List orchestration runs
      description: >-
        Returns orchestration runs the caller can access, optionally filtered by
        orchestration, by parent run, or by whether the run has a parent at all.


        Note when aggregating: a run's `usage` covers its whole subtree, so
        summing it over a list that contains both a parent and its children
        counts the children more than once. Pass `nested=false` to sum over
        runs a caller started.
      operationId: listOrchestrationRuns
      parameters:
        - name: orchestration_id
          in: query
          required: false
          description: Filter by orchestration public ID (orch_...)
          schema:
            type: string
        - name: parent_orchestration_run_id
          in: query
          required: false
          description: >-
            Filter to the runs one specific parent run's `loop` /
            `sub_orchestration` nodes started (run_...). This is how a caller
            holding a parent names the individual children behind its `usage`.
          schema:
            type: string
        - name: nested
          in: query
          required: false
          description: >-
            Filter by whether the run was started by another run. `false`
            returns only the runs a caller started (no parent), which is the
            set to sum `usage` over; `true` returns only the runs a `loop` /
            `sub_orchestration` node started, across every parent. Omit to
            return both.


            Contradicting `parent_orchestration_run_id` with `nested=false`
            is a `400`; any value other than `true` or `false` is a `400`.
          schema:
            type: boolean
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of runs
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/OrchestrationRun'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden

  /api/v1/orchestration-runs/{orchestration_run_id}/cancel:
    post:
      tags:
        - Orchestrations
      summary: Cancel an orchestration run
      description: Cancels a run that has not yet reached a terminal state.
      operationId: cancelOrchestrationRun
      parameters:
        - $ref: '#/components/parameters/orchestration_run_id'
      responses:
        '200':
          description: Cancelled run
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrationRun'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
        '409':
          description: Run is already in a terminal state

  /api/v1/orchestration-runs/{orchestration_run_id}/human-input:
    post:
      tags:
        - Orchestrations
      summary: Submit human input
      description: Provides human input to a run that is awaiting_input at a human node.
      operationId: submitHumanInput
      parameters:
        - $ref: '#/components/parameters/orchestration_run_id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HumanInputRequest'
      responses:
        '200':
          description: Run after processing human input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrationRun'
        '400':
          description: Invalid input
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
        '409':
          description: Run is not awaiting input

  /api/v1/orchestration-runs/{orchestration_run_id}/resume:
    post:
      tags:
        - Orchestrations
      summary: Resume an orchestration run
      description: >-
        Re-drives an awaiting_input orchestration run from its last checkpoint.
        This does not satisfy the pause itself — it carries no node_id or
        payload, so a run parked on a human or webhook-receive node re-parks on
        the same node. Use submit-human-input to supply the awaited payload and
        advance the run.
      operationId: resumeOrchestrationRun
      parameters:
        - $ref: '#/components/parameters/orchestration_run_id'
      responses:
        '200':
          description: Resumed run
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrationRun'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
        '409':
          description: Run is not awaiting input

  /api/v1/orchestration-runs/{orchestration_run_id}:
    get:
      tags:
        - Orchestrations
      summary: Get an orchestration run
      description: Returns the status, state, and artifacts of a specific run.
      operationId: getOrchestrationRun
      parameters:
        - $ref: '#/components/parameters/orchestration_run_id'
      responses:
        '200':
          description: Run details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrationRun'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  parameters:
    orchestration_id:
      in: path
      name: orchestration_id
      required: true
      schema:
        type: string
      description: Public ID of the orchestration (orch_...)
    orchestration_run_id:
      in: path
      name: orchestration_run_id
      required: true
      schema:
        type: string
      description: Public ID of the run (run_...)

  schemas:
    QueueStats:
      type: object
      description: A point-in-time snapshot of the orchestration run queue.
      properties:
        driver:
          type: string
          enum:
            - postgres
            - sqs
          description: >-
            The active queue driver (`ORCHESTRATION_QUEUE_DRIVER`). Under
            `sqs`, `oldest_queued_age_seconds` is always `null` and
            `per_project` is always empty — SQS exposes neither.
          example: postgres
        queue_depth:
          type: integer
          description: >-
            Tasks waiting to be claimed now (unclaimed and past their
            `available_at`). Backoff-delayed tasks are excluded.
          example: 12
        claimed_tasks:
          type: integer
          description: Tasks currently claimed with a valid (unexpired) lease.
          example: 3
        oldest_queued_age_seconds:
          type: number
          nullable: true
          description: >-
            Age in seconds of the oldest claimable-now task, or `null` when
            none are waiting.
          example: 4.2
        claim_latency_ms:
          type: object
          description: >-
            Claim-latency percentiles (time from a task becoming available to
            being claimed) over a rolling in-process window. `p50`/`p95` are
            `null` when no claim happened in the window.
          properties:
            p50:
              type: number
              nullable: true
              example: 18
            p95:
              type: number
              nullable: true
              example: 240
            window_seconds:
              type: integer
              example: 300
        per_project:
          type: array
          description: One row per project with any queued or claimed task.
          items:
            type: object
            properties:
              project_id:
                type: string
                description: Public project ID (proj_ prefix).
                example: proj_V1StGXR8Z5jdHi6B
              queued:
                type: integer
                example: 5
              claimed:
                type: integer
                example: 1
    OrchestrationNode:
      type: object
      description: A single execution unit in the orchestration graph.
      required:
        - id
        - type
      properties:
        id:
          type: string
          description: Unique node identifier within this orchestration.
        type:
          type: string
          description: >-
            Node execution type. Known types: agent, tool, transform,
            knowledge, memory_write, condition, human, approval, loop, poll,
            delay, webhook, emit_event, sub_orchestration. Open set — new types
            may be added in minor releases, and an unrecognized type is
            accepted at create time (the run fails when the node dispatches),
            so clients must tolerate unknown values.
        agent_id:
          x-soat-ref: agents
          type: string
          description: For agent nodes — public ID of the agent to invoke.
        tool_id:
          x-soat-ref: tools
          type: string
          description: For tool and poll nodes — public ID of the tool to call.
        operation_id:
          type: string
          description: For tool and poll nodes — specific operation/action on MCP/SOAT tools.
        expression:
          description: >-
            For transform/condition nodes — JSON Logic rule
            (https://jsonlogic.com) evaluated against the run state. A rule may
            be any JSON value (object, string, number, boolean, array), so no
            type is constrained.
        exit_condition:
          description: >
            For poll nodes — JSON Logic stop condition, evaluated each attempt
            against the run state augmented with `response` (the latest tool
            result) and `attempt` (1-based count); a truthy result stops
            polling.
        prompt:
          type: string
          description: For human nodes — prompt shown to the human reviewer.
        options:
          type: array
          items:
            type: string
          description: For human nodes — constrained choices.
        memory_id:
          x-soat-ref: memories
          type: string
          description: For memory_write nodes — public ID of the target memory.
        arguments:
          type: object
          additionalProperties: true
          description: >
            For approval nodes — input-mapping-style object (JSON Logic values)
            resolved against run state into the proposed tool call's arguments,
            frozen onto the created approval item.
        expires_in:
          type: integer
          description: >
            For approval nodes — seconds until the created approval item expires.
            Defaults to 86400 (24h) when omitted. An expired item can never
            execute; the run routes down its `on_expired` edge.
        instructions:
          type: string
          description: For approval nodes — optional guidance shown to the approver.
        reasoning:
          description: For approval nodes — JSON Logic (any JSON value) resolved into the item's reasoning.
        evidence:
          description: For approval nodes — JSON Logic (any JSON value) resolved into the item's evidence.
        predicted_impact:
          description: For approval nodes — JSON Logic (any JSON value) resolved into the item's predicted impact.
        input_mapping:
          type: object
          additionalProperties: true
          description: >
            Maps node input keys to values. Each value is JSON Logic
            (https://jsonlogic.com), the same evaluator used by transform and
            condition nodes. A single-key object is evaluated against the run
            state — `{"var": "key"}` reads `state.key`, `{"cat": [...]}` and
            `{">": [...]}` compute derived values. Any other value (string,
            number, boolean, array, multi-key object) is passed through as a
            literal.
        state_mapping:
          type: object
          additionalProperties: true
          description: >
            Maps state write paths to values. Each key is a `state.<path>`
            destination (the `state.` prefix is optional); each value is JSON
            Logic (https://jsonlogic.com) evaluated against
            `{ "output": <node artifact>, "state": <run state> }` — e.g.
            `{ "summary": {"var": "output.content"} }` writes the artifact's
            `content` field to `state.summary`. The same evaluator as
            input_mapping/transform/condition; only the context differs.
        output_schema:
          type: object
          description: For agent nodes — JSON Schema for structured output parsing.
        collection:
          type: string
          description: For loop nodes — state path to the collection to iterate over.
        item_variable:
          type: string
          description: For loop nodes — variable name injected into state for each item.
        parallelism:
          type: integer
          description: For loop nodes — number of items to process in parallel.
        context_keys:
          type: array
          nullable: true
          items:
            type: string
          description: >-
            For loop and sub_orchestration nodes — allowlist of the run's
            `tool_context` keys the child run inherits. When `null` (the
            default), the child inherits the parent's whole bag — the behavior
            of every graph authored before this field existed. When set, only
            the listed keys are handed down, so a run holding a broad
            credential can delegate one step to a shared sub-graph without
            passing on what that sub-graph does not need; `[]` hands down
            nothing. Matching is case-insensitive, since an entry names a key
            that becomes an HTTP header name; an entry outside that grammar is
            rejected at write time with `INVALID_TOOL_CONTEXT_KEY`. The
            server-derived identity keys (`session_id`, `actor_id`,
            `actor_external_id`) are unaffected — they are re-derived per
            generation in the child regardless of this list. Ignored for other
            node types.
        interval:
          type: string
          description: >
            For poll nodes — wait between attempts. Accepts a friendly suffix
            form (`5s`, `30s`, `5m`, `2h`, `500ms`) or ISO 8601 (e.g. PT5S).
        fail_on_timeout:
          type: boolean
          description: >
            For poll nodes — when max_iterations is reached without the exit
            condition becoming true, fail the run (true) instead of completing
            with condition_met=false (default false).
        duration:
          type: string
          description: >
            For delay nodes — how long to wait. Accepts a friendly suffix form
            (`5s`, `30s`, `5m`, `2h`, `500ms`) or ISO 8601 (e.g. PT5S).
        mode:
          type: string
          enum: [receive]
          description: >
            For webhook nodes — parks the run awaiting an inbound callback.
            `receive` is the only mode; to send a notification out of a graph,
            use an `emit_event` node instead.
        event_type:
          type: string
          description: >
            For emit_event nodes — the internal event type to emit (e.g.
            `guardrail.exception`). The node's input_mapping becomes the event
            `data`. Any Webhook subscribed to this event type in the run's
            project then delivers it — signed, retried, and tracked by the
            Webhooks module — so the graph holds no URL or secret of its own.
        orchestration_id:
          x-soat-ref: orchestrations
          type: string
          description: >
            Public ID of the orchestration this node runs — the child
            orchestration for sub_orchestration nodes, and the orchestration run
            once per item for loop nodes.
        max_iterations:
          type: integer
          description: >
            Maximum iterations before the node is aborted. For poll nodes this
            is the maximum number of attempts (default 10, ceiling 1000).
        retry:
          type: object
          description: >
            Retry-on-failure policy. When the node throws a transient error
            (unexpected/infrastructure errors and upstream 5xx) and attempts
            remain, the run parks as `sleeping` and re-executes the node after
            the backoff delay. Terminal errors (4xx business errors) fail
            immediately. Absent or `max_attempts <= 1` means fail-fast.
          properties:
            max_attempts:
              type: integer
              description: >
                Total attempts including the first (default 1, ceiling 20).
            backoff:
              type: object
              properties:
                strategy:
                  type: string
                  enum: [fixed, exponential]
                  description: >
                    `fixed` waits `delay_ms` between every attempt;
                    `exponential` doubles per prior attempt. Default `fixed`.
                delay_ms:
                  type: integer
                  description: Base delay between attempts in ms (default 1000).
                max_delay_ms:
                  type: integer
                  description: >
                    Cap on the computed backoff delay in ms (default 300000).

    OrchestrationEdge:
      type: object
      description: A directed connection between two nodes.
      required:
        - from
        - to
      properties:
        from:
          type: string
          description: Source node ID.
        to:
          type: string
          description: Target node ID.
        condition:
          type: string
          description: For condition node routing — label to match against condition output.
        activation_group:
          type: string
          description: Groups edges for join semantics.
        activation_condition:
          type: string
          enum: [all, any]
          description: Whether all or any edges in the activation group must fire.

    Orchestration:
      type: object
      required:
        - id
        - project_id
        - name
        - version
        - nodes
        - edges
        - created_at
        - updated_at
      properties:
        id:
          type: string
          description: Public ID (orch_...).
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the owning project.
        name:
          type: string
          description: Human-readable name.
        description:
          type: string
          nullable: true
          description: Optional description.
        version:
          type: integer
          description: >
            Incremented on every write that changes the graph; prior versions are
            archived. A run pins the version it started on, so these fields are a
            draft for runs started from now on rather than a live rewrite of the
            ones already executing.
          example: 1
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationNode'
        edges:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationEdge'
        state_schema:
          type: object
          nullable: true
          description: Optional JSON Schema for state validation.
        input_schema:
          type: object
          nullable: true
          description: Schema for run inputs (initial state).
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    CreateOrchestrationRequest:
      type: object
      required:
        - name
        - nodes
        - edges
      properties:
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the project. Optional when authenticating with a project-scoped API key, which defaults to the key's project; required otherwise.
        name:
          type: string
          description: Human-readable name.
        description:
          type: string
          nullable: true
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationNode'
        edges:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationEdge'
        state_schema:
          type: object
          nullable: true
        input_schema:
          type: object
          nullable: true
        version_label:
          type: string
          description: >-
            Optional tag for the version this create archives, e.g. `initial`.
          example: initial

    UpdateOrchestrationRequest:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationNode'
        edges:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationEdge'
        state_schema:
          type: object
          nullable: true
        input_schema:
          type: object
          nullable: true
        version_label:
          type: string
          description: >-
            Optional tag for the version this write archives, e.g. `pre-rewire`.
            Ignored when the write changes no graph field, since no version is
            archived.
          example: pre-rewire

    OrchestrationVersion:
      type: object
      description: >-
        An immutable archive of an orchestration's graph at one version.
      properties:
        id:
          type: string
          description: Public ID of the archived version
          example: orch_ver_V1StGXR8Z5jdHi6B
        orchestration_id:
          x-soat-ref: orchestrations
          type: string
          description: Public ID of the orchestration this version belongs to
          example: orch_V1StGXR8Z5jdHi6B
        version:
          type: integer
          description: The archived version number
          example: 1
        config:
          type: object
          additionalProperties: true
          description: >-
            The orchestration's versioned surface as it stood at this version:
            `nodes`, `edges`, `state_schema` and `input_schema`. Name and
            description are metadata — bumping the version when one of them changes
            would make two version numbers denote the same topology, which is
            exactly what a run cites.


            Deliberately open rather than a fixed schema: an archive written by an
            earlier release of SOAT reflects the orchestration surface **of its own
            time**, so it may carry fields the current API no longer documents.
          properties:
            nodes:
              type: array
              items:
                $ref: '#/components/schemas/OrchestrationNode'
            edges:
              type: array
              items:
                $ref: '#/components/schemas/OrchestrationEdge'
            state_schema:
              type: object
              nullable: true
            input_schema:
              type: object
              nullable: true
        label:
          type: string
          nullable: true
          description: >-
            Optional human tag for this version, e.g. `pre-rewire`. Set from the
            `version_label` field of a write, the `label` field of a restore, or
            generated for one.
          example: restored from v2
        created_by:
          x-soat-ref: users
          type: string
          nullable: true
          description: >-
            Public ID of the user whose action produced this version. Null for
            writes with no request user behind them.
        created_at:
          type: string
          format: date-time

    RestoreOrchestrationVersionRequest:
      type: object
      properties:
        label:
          type: string
          description: >-
            Optional tag for the version the restore creates. Defaults to
            `restored from v<version>`.
          example: rollback to pre-incident graph

    OrchestrationRun:
      type: object
      required:
        - id
        - orchestration_id
        - project_id
        - status
        - state
        - active_nodes
        - artifacts
        - created_at
        - updated_at
      properties:
        id:
          type: string
          description: Public ID (run_...).
        orchestration_id:
          x-soat-ref: orchestrations
          type: string
          description: Public ID of the parent orchestration.
        orchestration_version:
          type: integer
          nullable: true
          description: >
            The orchestration version this run executes, fixed when the run
            started. Every later step of the run — the first drive, a wake from
            `sleeping`, a human or approval resume, a redrive after a crash —
            resolves the graph from this version, so editing the orchestration
            never re-shapes a run already in flight. Fetch the graph it names at
            `GET /api/v1/orchestrations/{orchestration_id}/versions/{version}`.


            Null for runs created before pinning existed, which execute the live
            graph.
          example: 3
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the owning project.
        status:
          type: string
          description: >-
            Run lifecycle state. `queued` awaits a worker; `running` is actively
            executing; `sleeping` is parked on a delay/poll wait (no worker);
            `awaiting_input` is parked on a human node; `succeeded`/`failed`/
            `cancelled` are terminal; `expired` is a wait that passed its
            deadline.
          enum:
            [
              queued,
              running,
              sleeping,
              awaiting_input,
              succeeded,
              failed,
              cancelled,
              expired,
            ]
        state:
          type: object
          description: Current accumulated state.
        active_nodes:
          type: array
          items:
            type: string
          description: Node IDs currently active.
        artifacts:
          type: object
          description: Map of node ID to output artifact.
        error:
          type: object
          nullable: true
          description: Error details when status is failed.
        trace_id:
          x-soat-ref: traces
          type: string
          nullable: true
        input:
          type: object
          nullable: true
          description: Initial input provided at run creation.
        tool_context:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: >-
            The `tool_context` supplied at run creation, forwarded as
            `X-Soat-Context-<key>` headers on every tool call the run's agent
            nodes make. Null when the run was started without one.
        metadata:
          type: object
          additionalProperties: true
          nullable: true
          description: >-
            The caller-owned key/value metadata supplied at run creation,
            returned verbatim. Null when the run was started without any. The
            server writes nothing here and no key is reserved; the bag is never
            merged into `state`, so nothing in it reaches the graph.
          example:
            tenant_account_id: '42'
            dispatch_batch: nightly-2026-08-25
        parent_orchestration_run_id:
          x-soat-ref: orchestration-runs
          type: string
          nullable: true
          description: >-
            The run whose node started this one — set only on a child a `loop` or
            `sub_orchestration` node spawned, null for a run a caller started. A
            child is its own run with its own usage events, so this is what makes
            a delegated run's spend attributable to the run that ordered it.
        parent_node_id:
          type: string
          nullable: true
          description: >-
            The node within `parent_orchestration_run_id` that started this run.
            Null when `parent_orchestration_run_id` is null.
        run_depth:
          type: integer
          minimum: 0
          description: >-
            `loop` / `sub_orchestration` edges between this run and the run a
            caller started: `0` for a caller-started run, one more than its
            parent's for a child. Starting a child past the effective bound —
            the smaller of the deployment's `MAX_ORCHESTRATION_RUN_DEPTH`
            (default 10) and the project's `max_run_depth` — is refused with
            `ORCHESTRATION_RUN_DEPTH_LIMIT`, which fails the run that tried to
            descend. That bounds a graph whose `sub_orchestration` node names
            itself, directly or through a cycle of two graphs, which the
            intra-graph cycle check cannot see.
          example: 0
        output:
          type: object
          nullable: true
          description: Terminal node artifact(s) when the run has succeeded.
        node_executions:
          type: array
          description: >-
            Per-node execution records in chronological order. Each entry
            captures the resolved input, output, status, and error for a single
            node execution — the orchestration analogue of an LLM trace.
          items:
            $ref: '#/components/schemas/NodeExecution'
        usage:
          allOf:
            - $ref: '#/components/schemas/RunUsageTotals'
          description: >-
            What the run cost: token counts and `cost_usd` summed across every
            metered generation it produced **and every run it started** through
            `loop` / `sub_orchestration` nodes, at any depth. Present on the
            single-run read; omitted from run list responses.


            A nested child is a run record of its own, so this figure spans
            several of them. Two consequences: summing `usage` across a list
            that mixes parents and children double-counts (filter with
            `nested=false`), and the per-event receipt at
            `/api/v1/usage/receipt` stays scoped to one run — its line items
            carry a `node_id` from one graph only.
        usage_own:
          allOf:
            - $ref: '#/components/schemas/RunUsageTotals'
          description: >-
            The same roll-up restricted to **this run's own nodes**, excluding
            every nested run it started. Equal to `usage` for a run with no
            children; below it for a run that delegates. Present on the
            single-run read; omitted from run list responses.


            This is the field to read to see where cost sits in a run tree —
            own versus subtree — without walking the children.
        required_action:
          allOf:
            - $ref: '#/components/schemas/RequiredAction'
          nullable: true
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    RunUsageTotals:
      type: object
      description: >-
        Token and cost roll-up for an orchestration run, summed across every
        usage event the run's generations produced.
      properties:
        total_input_tokens:
          type: integer
        total_output_tokens:
          type: integer
        total_cached_tokens:
          type: integer
        total_reasoning_tokens:
          type: integer
        total_cost_usd:
          type: number
          nullable: true
          description: >-
            Sum of the run's priced component costs in USD. Null when nothing on
            the run was priced.

    NodeExecution:
      type: object
      description: >-
        Record of a single node execution within a run, used to debug which
        node failed, what input it received, and what it produced.
      required:
        - node_id
        - attempt
        - status
        - created_at
      properties:
        node_id:
          type: string
          description: ID of the executed node.
        node_type:
          type: string
          nullable: true
          description: Type of the executed node (e.g. agent, transform).
        attempt:
          type: integer
          description: >
            1-based attempt number. A node with a retry policy produces one
            record per attempt (failed attempts followed by a final record).
        status:
          type: string
          enum: [running, completed, failed, requires_action, skipped]
          description: >-
            Node execution status. `running` marks an execution record whose
            node is still in flight. Open set — new statuses may be added in
            minor releases; clients must tolerate unknown values.
        input:
          type: object
          nullable: true
          description: Resolved input_mapping the node received.
        output:
          type: object
          nullable: true
          description: Output artifact the node produced (null when failed).
        error:
          type: object
          nullable: true
          description: Error details when status is failed.
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    RequiredAction:
      type: object
      description: Details for an awaiting_input run waiting for human input.
      required:
        - type
        - node_id
        - prompt
        - context
      properties:
        type:
          type: string
          enum:
            - human_input
            - webhook_receive
            - approval
          description: >-
            Discriminator identifying the kind of pause. Open enum — new pause
            kinds may be added in minor releases; clients must tolerate unknown
            values.
        node_id:
          type: string
        prompt:
          type: string
        context:
          type: object
        options:
          type: array
          items:
            type: string
          nullable: true
        approval_spec:
          type: object
          description: >-
            Present only for `approval` pauses — the frozen tool proposal the
            engine emits as an ApprovalItem when the run parks. Copied as a
            value; inner keys stay exactly as authored.
        approval_id:
          x-soat-ref: approvals
          type: string
          description: Present once the approval item is emitted.
        expires_at:
          type: string
          format: date-time
          description: Present for `approval` pauses — when the item expires.

    HumanInputRequest:
      type: object
      required:
        - node_id
      properties:
        node_id:
          type: string
          description: ID of the human node to satisfy.
        output:
          type: object
          description: Output/response provided by the human reviewer.

    StartRunRequest:
      type: object
      required:
        - orchestration_id
      properties:
        orchestration_id:
          x-soat-ref: orchestrations
          type: string
          description: Orchestration to run (orch_...).
          example: orch_V1StGXR8Z5jdHi6B
        input:
          type: object
          description: Initial state for the run (merged with orchestration defaults).
        tool_context:
          type: object
          additionalProperties:
            type: string
          description: >-
            Key-value pairs forwarded as `X-Soat-Context-<key>` headers on every
            `http`, `mcp` and `builtin` tool call made by an agent node of this run
            — including the agents of any child run a `loop` or
            `sub_orchestration` node starts. The header name is
            `X-Soat-Context-` plus the key verbatim; no character is re-cased.


            The bag is stored on the run and re-read on every step, so it
            survives an `awaiting_input` pause, a `sleeping` wait, a background
            worker drive and a crash redrive. A key that is not a valid HTTP
            header name, or two keys that map to the same header, are rejected
            with `400 INVALID_TOOL_CONTEXT_KEY` and no run is created.


            The reserved identity keys (`session_id`, `actor_id`,
            `actor_external_id`) are stripped at generation time — a caller cannot
            address them from here.
          example:
            ocaToken: eyJhbGciOiJIUzI1NiJ9.abc
        metadata:
          type: object
          additionalProperties: true
          description: >-
            Caller-supplied key/value metadata attached to the run record for
            per-run attribution (e.g. which of your own tenants this run belongs
            to, or the dispatch batch that started it). Round-trips verbatim on
            every read of the run, on the list as well as the single read.


            The bag is caller-owned and no key is reserved: server-owned state
            (status, the pinned orchestration version, the trace, usage,
            artifacts, the run's own `input` and accumulated `state`) lives in
            its own top-level field and cannot be written from here.


            It is **not** merged into run state: no graph node sees it, and an
            `input_schema` never has to tolerate it — which is what makes it the
            place for an infrastructural label, rather than `input`. Keys are
            never transformed. It is not inherited by the child runs a `loop` or
            `sub_orchestration` node starts; each child carries whatever the
            graph gives it, which today is nothing.
          example:
            tenant_account_id: '42'
            dispatch_batch: nightly-2026-08-25
        wait:
          type: boolean
          default: false
          description: >-
            When true, block until the run reaches a terminal (succeeded/failed)
            or awaiting_input state and return the settled run. When false
            (default), return immediately with status "queued" and execute the
            run in the background.

    ValidateOrchestrationRequest:
      type: object
      properties:
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationNode'
        edges:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationEdge'
        input_schema:
          type: object
          nullable: true
          description: Optional JSON Schema for run inputs; its top-level properties seed state.

    ValidationError:
      type: object
      properties:
        path:
          type: string
          description: Location of the issue (e.g. nodes[1].input_mapping.val).
        message:
          type: string
          description: Human-readable description of the issue.

    ValidationResult:
      type: object
      required:
        - valid
        - errors
        - warnings
      properties:
        valid:
          type: boolean
          description: True when there are no blocking errors.
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ValidationError'
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/ValidationError'
