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
        - $ref: './pagination.yaml#/components/parameters/Limit'
        - $ref: './pagination.yaml#/components/parameters/Offset'
      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 `problems` and non-blocking
        `warnings` (e.g. a state key only written on a conditional branch). The
        same checks are enforced on create and update, which fail with `400
        ORCHESTRATION_VALIDATION_FAILED` listing the same items under
        `meta.problems` and `meta.warnings`.
      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/OrchestrationValidationResult'
        '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. The per-project breakdown is listed by
        `GET /api/v1/orchestrations/queue/stats/projects`. Intended for
        admin/operator policies; guarded by `orchestrations:GetQueueStats`.

        Every figure is scoped to what the caller may see. A project-scoped
        caller gets `queue_depth` and `claimed_tasks` summed over their own
        projects, and `null` for
        `oldest_queued_age_seconds` and the `claim_latency_ms` percentiles —
        both describe the whole deployment and cannot be narrowed, so they are
        withheld rather than approximated. An unrestricted caller (the action
        granted on every project) gets the deployment-wide figures.
      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/queue/stats/projects:
    get:
      tags:
        - Orchestrations
      summary: List the projects with queued orchestration work
      description: >
        One row per project with any queued or claimed task, ordered by project
        ID: the per-project breakdown of `GET /api/v1/orchestrations/queue/stats`.
        A project-scoped caller sees its own projects only. Guarded by
        `orchestrations:GetQueueStats`.
      operationId: listQueueStatsProjects
      security:
        - bearerAuth: []
      parameters:
        - $ref: './pagination.yaml#/components/parameters/Limit'
        - $ref: './pagination.yaml#/components/parameters/Offset'
      responses:
        '200':
          description: A page of projects with queued or claimed work
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/QueueProjectStats'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '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
      x-soat-resource:
        kind: orchestration
        from: orchestration_id
      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
      x-soat-resource:
        kind: orchestration
        from: orchestration_id
      parameters:
        - $ref: '#/components/parameters/orchestration_id'
        - $ref: './concurrency.yaml#/components/parameters/IfMatchVersion'
      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
        '409':
          $ref: './concurrency.yaml#/components/responses/VersionConflict'

    delete:
      tags:
        - Orchestrations
      summary: Delete an orchestration
      description: Deletes an orchestration definition and all its runs.
      operationId: deleteOrchestration
      x-soat-resource:
        kind: orchestration
        from: orchestration_id
      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
      x-soat-resource:
        kind: orchestration
        from: orchestration_id
      parameters:
        - $ref: '#/components/parameters/orchestration_id'
        - $ref: './pagination.yaml#/components/parameters/Limit'
        - $ref: './pagination.yaml#/components/parameters/Offset'
      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
      x-soat-resource:
        kind: orchestration
        from: orchestration_id
      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
      x-soat-resource:
        kind: orchestration
        from: orchestration_id
      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
        '409':
          $ref: './concurrency.yaml#/components/responses/VersionConflict'

  /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/completed/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
      parameters:
        - $ref: './idempotency.yaml#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StartOrchestrationRunRequest'
      responses:
        '200':
          description: >-
            Duplicate request — the run the `idempotency_key` already names is
            returned and no second run is started.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrationRun'
        '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, `metadata` that is not a JSON object, or an
            `idempotency_key` that is not a non-empty string of at most 255
            characters). No run is created.
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Orchestration not found
        '409':
          description: >-
            `IDEMPOTENCY_KEY_REUSED` — the key is already claimed by a run
            started from a different request. No run is created.

    get:
      tags:
        - Orchestrations
      summary: List orchestration runs
      description: >-
        Returns orchestration runs the caller can access, optionally filtered by
        orchestration, by parent run, by status, 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: project_id
          in: query
          required: false
          description: Narrow the listing to one project (proj_...). A project-scoped key lists its own project without it.
          schema:
            type: string
        - 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: status
          in: query
          required: false
          description: >-
            Filter by run status. Repeat the parameter to OR values —
            `status=queued&status=running&status=sleeping&status=awaiting_input`
            is the set still driving, which is how a caller finds live work
            without paging every run the project ever started.


            There is no `non_terminal` shorthand on purpose: which statuses
            count as live is the caller's policy. A value outside the enum,
            empty string included, is a `400`.
          schema:
            type: array
            items:
              type: string
              enum:
                - queued
                - running
                - sleeping
                - awaiting_input
                - completed
                - failed
                - cancelled
                - expired
          style: form
          explode: true
          example:
            - queued
            - running
        - $ref: './pagination.yaml#/components/parameters/Limit'
        - $ref: './pagination.yaml#/components/parameters/Offset'
      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
      x-soat-resource:
        kind: orchestration_run
        from: orchestration_run_id
      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}/pause:
    post:
      tags:
        - Orchestrations
      summary: Pause an orchestration run
      description: >-
        Parks a run in flight as `awaiting_input` at its next checkpoint, with a
        `required_action` of type `paused` naming the pause as operator-initiated
        rather than a node's. Unlike cancel, the run keeps its last checkpoint
        and resume-orchestration-run re-drives it from there — so work already
        done is deferred rather than discarded.

        A `queued` or `sleeping` run is parked immediately (a sleeping run keeps
        the wake it was due, and resuming hands it back to the scheduler at that
        instant). A `running` run keeps running until the round in flight
        reaches its checkpoint, so the response may still read `running` while
        `pause_requested_at` is set. A run already parked on a human, webhook or
        approval node keeps that node's `required_action`; the pause is still
        recorded, which is what makes submit-human-input refuse until the run is
        resumed.

        The pause fans out to the run's `loop` / `sub_orchestration` descendants
        — each parks at its own next checkpoint — because otherwise a parent's
        pause would bound nothing. Resuming does not fan out: each parked
        descendant is resumed by its own id.

        Idempotent: pausing an already-paused run answers with it unchanged.
      operationId: pauseOrchestrationRun
      x-soat-resource:
        kind: orchestration_run
        from: orchestration_run_id
      parameters:
        - $ref: '#/components/parameters/orchestration_run_id'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PauseOrchestrationRunRequest'
      responses:
        '200':
          description: Paused run
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrationRun'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
        '409':
          description: Run has already settled

  /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
      x-soat-resource:
        kind: orchestration_run
        from: orchestration_run_id
      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, or an operator pause is in force

  /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.

        It is also the only thing that lifts an operator pause
        (pause-orchestration-run): a run parked with `required_action.type` of
        `paused` re-drives the frontier that had not run yet, and one paused
        mid-timer goes back to `sleeping` for the wake it was already due.
      operationId: resumeOrchestrationRun
      x-soat-resource:
        kind: orchestration_run
        from: orchestration_run_id
      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
      x-soat-resource:
        kind: orchestration_run
        from: orchestration_run_id
      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

  /api/v1/orchestration-runs/{orchestration_run_id}/node-executions:
    get:
      tags:
        - Orchestrations
      summary: List a run's node executions
      description: >-
        The run's per-node execution records, oldest first: one per node
        attempt, retries and skipped nodes included. Each captures the resolved
        input, output, status and error of that attempt — the orchestration
        analogue of an LLM trace. Reading them is reading the run
        (`orchestrations:GetRun`).
      operationId: listOrchestrationRunNodeExecutions
      x-soat-resource:
        kind: orchestration_run
        from: orchestration_run_id
      parameters:
        - $ref: '#/components/parameters/orchestration_run_id'
        - $ref: './pagination.yaml#/components/parameters/Limit'
        - $ref: './pagination.yaml#/components/parameters/Offset'
      responses:
        '200':
          description: A page of the run's node executions
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/NodeExecution'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '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 the
            per-project listing 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. Summed over
            the caller's own projects when the caller is project-scoped.
          example: 12
        claimed_tasks:
          type: integer
          description: >-
            Tasks currently claimed with a valid (unexpired) lease. Summed over
            the caller's own projects when the caller is project-scoped.
          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. Also `null` for a project-scoped caller: the
            figure is deployment-wide.
          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, and for a
            project-scoped caller: the window is deployment-wide.
          properties:
            p50:
              type: number
              nullable: true
              example: 18
            p95:
              type: number
              nullable: true
              example: 240
            window_seconds:
              type: integer
              example: 300
    QueueProjectStats:
      type: object
      description: One project's share of the orchestration run queue.
      properties:
        project_id:
          type: string
          description: Public project ID (proj_ prefix).
          example: proj_V1StGXR8Z5jdHi6B
        queued:
          type: integer
          description: Tasks of this project waiting to be claimed now.
          example: 5
        claimed:
          type: integer
          description: Tasks of this project claimed with a valid lease.
          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, 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.
        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.
            An `agent` node's artifact always carries both `content` (the text
            response) and `object` (the parsed value when a schema applied,
            `null` otherwise), so `output.content` reads the same whether or
            not the node declares an `output_schema`.
        output_schema:
          type: object
          description: >
            For agent nodes — JSON Schema the model's answer is parsed into,
            reaching the artifact as `object`. Declaring it here is only needed
            for an agent that carries no `output_schema` of its own: the
            agent's schema already produces the artifact's `object` wherever
            that agent generates.
        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.
        on_item_error:
          type: string
          enum: [fail, collect]
          description: >-
            For loop nodes — what a failed item does. `fail` (the default)
            fails the node on the first item whose child run settles
            `failed`, `cancelled` or `expired`. `collect` keeps going: that
            item's entry in `results` becomes `{ "error": { "code",
            "message" }, "orchestration_run_id" }`, in its original position.
            Either way the artifact carries `failed_count`. A child run that
            could not be started (for example past the nesting depth bound)
            fails the node in both modes.
        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. 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.
          additionalProperties: false
          properties:
            max_attempts:
              type: integer
              description: >
                Total attempts including the first (default 1, ceiling 20).
            backoff:
              type: object
              additionalProperties: false
              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 come from a
            completed node before the target runs. Omitted, `all`.

    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).
        output_mapping:
          $ref: '#/components/schemas/OrchestrationOutputMapping'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    OrchestrationOutputMapping:
      type: object
      nullable: true
      additionalProperties: true
      description: >-
        The shape of a completed run's `output`, owned by the orchestration
        rather than by its terminal node ids. Each key is an output field (a
        dotted key such as `summary.title` builds a nested object); each value
        is JSON Logic (https://jsonlogic.com) evaluated against
        `{ "state": <final run state> }`, which includes `state.input` and
        `state.nodes.<id>`. A missing path maps to `null`; a mapping that
        throws fails the run. Null (or omitted) keys `output` by terminal node
        id. Versioned with the graph; `null` on update clears it.
      example:
        summary: { var: state.summary }
        answer: { var: state.nodes.answer.content }

    CreateOrchestrationRequest:
      type: object
      required:
        - name
        - nodes
        - edges
      additionalProperties: false
      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:
          description: >-
            The graph's nodes. Each `id` is unique and is what edges and `nodes.<id>`
            state references name; a node with no incoming edge starts the run.
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationNode'
        edges:
          description: >-
            Directed edges between node `id`s. Every `from`/`to` must name a node in
            `nodes`, and the graph must be acyclic unless it contains a `loop` node. A
            node with several outgoing edges activates every target in parallel. `[]`
            for a graph of independent nodes.
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationEdge'
        state_schema:
          type: object
          nullable: true
        input_schema:
          type: object
          nullable: true
        output_mapping:
          $ref: '#/components/schemas/OrchestrationOutputMapping'
        version_label:
          type: string
          description: >-
            Optional tag for the version this create archives, e.g. `initial`.
          example: initial

    UpdateOrchestrationRequest:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        nodes:
          description: >-
            The graph's nodes. Each `id` is unique and is what edges and `nodes.<id>`
            state references name; a node with no incoming edge starts the run. Replaces
            the stored list; omitted, the stored nodes are kept and validated against
            the new `edges`.
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationNode'
        edges:
          description: >-
            Directed edges between node `id`s. Every `from`/`to` must name a node in
            `nodes`, and the graph must be acyclic unless it contains a `loop` node. A
            node with several outgoing edges activates every target in parallel.
            Replaces the stored list; omitted, the stored edges are kept and validated
            against the new `nodes`.
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationEdge'
        state_schema:
          type: object
          nullable: true
        input_schema:
          type: object
          nullable: true
        output_mapping:
          $ref: '#/components/schemas/OrchestrationOutputMapping'
        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

        expected_version:
          description: >-
            Refuses the write unless the resource is at this version.
          allOf:
            - $ref: './concurrency.yaml#/components/schemas/ExpectedVersion'
    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
            output_mapping:
              $ref: '#/components/schemas/OrchestrationOutputMapping'
        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_principal_type:
          nullable: true
          allOf:
            - $ref: './principals.yaml#/components/schemas/PrincipalType'
          description: >-
            Who produced this version. Null for a write with no principal behind
            it.
        created_by_principal_id:
          type: string
          nullable: true
          description: >-
            Public id of that principal: the user (`user_…`), or for API key
            auth the key itself (`key_…`), so the record names which key acted.
          example: user_V1StGXR8Z5jdHi6B
        created_at:
          type: string
          format: date-time

    RestoreOrchestrationVersionRequest:
      type: object
      additionalProperties: false
      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; `completed`/`failed`/
            `cancelled` are terminal; `expired` is a wait that passed its
            deadline.
          enum:
            [
              queued,
              running,
              sleeping,
              awaiting_input,
              completed,
              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:
          allOf:
            - $ref: './errors.yaml#/components/schemas/StoredError'
          nullable: true
          description: Why it failed, 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.
        metadata:
          description: >-
            The caller-owned key/value metadata supplied at run creation,
            returned verbatim. Absent 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
          allOf:
            - $ref: './metadata.yaml#/components/schemas/MetadataBag'
        idempotency_key:
          type: string
          nullable: true
          description: >-
            The deduplication key the run was started under, unique within the
            project and claimed for as long as the run record exists. Null for a
            run started without one, which every platform-started run is.
          example: dispatch-2026-09-18-activation-42
        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.
        orchestration_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_orchestration_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: >-
            What a completed run returns: the orchestration's `output_mapping`
            evaluated over the final state, or, when it declares none, the
            terminal node artifacts keyed by node id. Null unless the run
            completed.
        usage:
          allOf:
            - $ref: './usage-totals.yaml#/components/schemas/UsageTotals'
          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 only on
            the single-run read (`GET /api/v1/orchestration-runs/{orchestration_run_id}`);
            omitted from run lists and from every write's response, including
            a start with `wait: true`.


            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: './usage-totals.yaml#/components/schemas/UsageTotals'
          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 only on the
            single-run read, like `usage`.


            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
        pause_requested_at:
          type: string
          format: date-time
          nullable: true
          description: >-
            When an operator pause was requested, or null when none is in force.
            Independent of `status`: a `running` run keeps running until its next
            checkpoint, and a run parked on a node keeps that node's
            `required_action`.
        pause_reason:
          type: string
          nullable: true
          description: The reason supplied with the pause, when one was.
        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

    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
        - dispatches
        - 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).
        dispatches:
          type: integer
          description: >
            How many times this attempt was dispatched, including the first.
            A worker that stops mid-node leaves its task to be redelivered, and
            the redelivery re-runs the node under this same record — so a count
            above 1 is work the run issued, and was billed for, more than once.
        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:
          allOf:
            - $ref: './errors.yaml#/components/schemas/StoredError'
          nullable: true
          description: Why it failed, 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: >-
        Why an awaiting_input run is parked. `node_id`, `prompt` and `context`
        describe a node's own pause and are present for every kind but `paused`,
        which is an operator pause with no node of its own and carries `reason`
        instead.
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - human_input
            - webhook_receive
            - approval
            - paused
          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
          description: >-
            The node the run is parked at. Absent for a `paused` action, which
            no node produced.
        prompt:
          type: string
        context:
          type: object
        reason:
          type: string
          nullable: true
          description: >-
            Present for `paused` — the reason the operator gave, or null when
            they gave none.
        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.

    PauseOrchestrationRunRequest:
      type: object
      properties:
        reason:
          type: string
          maxLength: 256
          description: >-
            Why the run is being paused, surfaced on the parked run's
            `required_action.reason` and on `pause_reason`.
      additionalProperties: false

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

    StartOrchestrationRunRequest:
      type: object
      required:
        - orchestration_id
      additionalProperties: false
      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. It is **write-only**: a run is a
            record every principal who may read runs can read, and a credential
            in it is not theirs to see, so it is never returned on one. It also
            does not outlive the run: reaching a terminal status (`completed`,
            `failed`, `cancelled` or `expired`) clears it. 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:
          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
          allOf:
            - $ref: './metadata.yaml#/components/schemas/MetadataBag'
        idempotency_key:
          type: string
          maxLength: 255
          description: >-
            Deduplication key, unique within the project, that makes a retry of
            an ambiguous failure safe. The first request under a key starts the
            run and answers `201`; any later request carrying the same key
            answers `200` with that same run, whatever state it has reached —
            including a retry that arrives while the original is still running.


            The key is claimed by the run record and stays claimed for as long
            as the record exists, so it never silently expires and lets a
            second run through.


            `orchestration_id`, `input`, `tool_context` and `metadata` are the
            request the key names: reusing a key with any of them changed is
            `409 IDEMPOTENCY_KEY_REUSED` rather than a replay of a run that
            does something else. `wait` is not part of that comparison — it
            says how the caller waits, not what the run is — so a retry may
            flip it.
          example: dispatch-2026-09-18-activation-42
        wait:
          type: boolean
          default: false
          description: >-
            When true, block until the run reaches a terminal (completed/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
      additionalProperties: false
      properties:
        nodes:
          description: >-
            The graph's nodes. Each `id` is unique and is what edges and `nodes.<id>`
            state references name; a node with no incoming edge starts the run. Checked
            exactly as create checks them, without persisting.
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationNode'
        edges:
          description: >-
            Directed edges between node `id`s. Every `from`/`to` must name a node in
            `nodes`, and the graph must be acyclic unless it contains a `loop` node. A
            node with several outgoing edges activates every target in parallel. Checked
            exactly as create checks them, without persisting.
          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.

    OrchestrationValidationResult:
      type: object
      required:
        - valid
        - problems
        - warnings
      properties:
        valid:
          type: boolean
          description: True when there are no blocking problems.
        problems:
          type: array
          description: >-
            Every blocking problem — the items a refused write lists under
            `meta.problems`.
          items:
            $ref: './errors.yaml#/components/schemas/ValidationProblem'
        warnings:
          type: array
          description: >-
            Problems that do not block a write, such as a state key written
            only on a conditional branch (`CONDITIONAL_STATE_KEY`). A refused
            write carries them under `meta.warnings`.
          items:
            $ref: './errors.yaml#/components/schemas/ValidationProblem'
