openapi: 3.0.3
info:
  title: SOAT Workflows API
  version: 1.0.0
  description: >-
    API for managing workflows — state-machine definitions (named states,
    allowed transitions, guards, and per-state automation) that tasks live in.
    An orchestration is a pipeline that ends; a workflow is a state graph a task
    lives in.
  contact:
    name: SOAT Team
    url: https://github.com/ttoss/soat
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: Workflows
    description: Manage workflow definitions
security:
  - bearerAuth: []
paths:
  /api/v1/workflows:
    get:
      description: Lists workflow definitions in a project.
      tags:
        - Workflows
      summary: List workflows
      operationId: listWorkflows
      parameters:
        - name: project_id
          in: query
          required: false
          schema:
            type: string
        - 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: A list of workflows
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Workflow'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
    post:
      description: Creates a new workflow definition. The definition is statically validated.
      tags:
        - Workflows
      summary: Create a workflow
      operationId: createWorkflow
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWorkflowRequest'
      responses:
        '201':
          description: Workflow created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workflow'
        '400':
          description: Bad request (invalid definition)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '409':
          description: A workflow with this name already exists

  /api/v1/workflows/{workflow_id}:
    get:
      description: Retrieves a workflow definition.
      tags:
        - Workflows
      summary: Get a workflow
      operationId: getWorkflow
      parameters:
        - name: workflow_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Workflow details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workflow'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Workflow not found
    patch:
      description: >-
        Updates a workflow definition. Structural changes (states/transitions)
        are re-validated. Existing tasks in a removed state stay put but can only
        leave via transitions valid in the new definition.
      tags:
        - Workflows
      summary: Update a workflow
      operationId: updateWorkflow
      parameters:
        - name: workflow_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWorkflowRequest'
      responses:
        '200':
          description: Workflow updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workflow'
        '400':
          description: Bad request (invalid definition)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Workflow not found
    delete:
      description: Deletes a workflow. Rejected while open tasks exist.
      tags:
        - Workflows
      summary: Delete a workflow
      operationId: deleteWorkflow
      parameters:
        - name: workflow_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Workflow deleted
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Workflow not found
        '409':
          description: The workflow has open tasks and cannot be deleted

  /api/v1/workflows/{workflow_id}/versions:
    get:
      description: >
        Returns the workflow's archived state machines, newest first. A version is
        written on create and on every subsequent write that changes the
        definition (`states`, `transitions`, `payload_schema`) — through the REST
        API or a formation apply alike. Metadata-only edits (name, description) do
        not archive a version. See
        [Versioning](/docs/modules/workflows#versioning).
      tags:
        - Workflows
      summary: List a workflow's versions
      operationId: listWorkflowVersions
      parameters:
        - name: workflow_id
          in: path
          required: true
          schema:
            type: string
        - 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 workflow versions, newest first
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WorkflowVersion'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Workflow not found

  /api/v1/workflows/{workflow_id}/versions/{version}:
    get:
      description: >
        Returns the exact state machine a given version describes. Every task
        records the version it entered on in `workflow_version` and runs on that
        machine for its whole life, so this is how you read the definition a task
        is actually being validated against — including a task whose workflow has
        been rewired since.
      tags:
        - Workflows
      summary: Fetch an archived workflow version
      operationId: getWorkflowVersion
      parameters:
        - name: workflow_id
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          description: The archived version number
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Archived workflow version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowVersion'
        '400':
          description: Bad request — version is not a positive integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found

  /api/v1/workflows/{workflow_id}/versions/{version}/restore:
    post:
      description: >
        Writes an archived version's state machine back as the workflow's live
        definition, which archives it again as a **new** version rather than
        rewinding the counter — so a task pinned to any version in between still
        runs on the machine it entered on.


        The restore runs through the ordinary update path, so the archived
        definition goes through the same validation as an authored one. That
        includes resolving every `on_enter` dispatch target, so restoring a
        version whose agent or orchestration has since been deleted fails with
        `400` rather than writing a definition that would strand a task on entry.
        Restoring the definition the workflow already holds is a no-op and
        archives nothing. Tasks already in flight are unaffected either way — a
        restore is an ordinary edit, and pinning is what keeps it from reaching
        them.
      tags:
        - Workflows
      summary: Restore an archived workflow state machine
      operationId: restoreWorkflowVersion
      parameters:
        - name: workflow_id
          in: path
          required: true
          schema:
            type: string
        - 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/RestoreWorkflowVersionRequest'
      responses:
        '200':
          description: The workflow, at its new version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workflow'
        '400':
          description: Bad request — version is not a positive integer, or the restored definition is invalid
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  schemas:
    WorkflowState:
      type: object
      description: >-
        A named state. Exactly one state must be `initial: true`; any number may
        be `terminal: true`. A `kind: human` state never dispatches — the task
        parks until a transition fires. `on_enter` (§5) dispatches exactly one
        of an agent generation (`kind: agent`, `agent_id`), an orchestration run
        (`kind: orchestration`, `orchestration_id`) or a tool call
        (`kind: tool`, `tool_id`, optional `operation_id`) on entry, optionally
        under a `retry` policy (`max_attempts` 1-10, `backoff_seconds`,
        `backoff_multiplier`) that re-runs execution failures before
        `on_failure` applies. A `tool` dispatch settles within the dispatch and
        is adjudicated by the same guardrails as an orchestration `tool` node;
        for anything that must wait (a delay, a poll, a multi-step pipeline, or
        an approval-gated tool), dispatch an orchestration instead.
      additionalProperties: true
      required:
        - name
      properties:
        name:
          type: string
        initial:
          type: boolean
        terminal:
          type: boolean
        kind:
          type: string
          description: >-
            `human` marks a human-in-the-loop parking state: the state never
            dispatches (declaring `on_enter` on it is rejected at validation)
            and the task parks until a principal fires a transition.
        stalled_after:
          type: integer
          nullable: true
          description: >-
            Seconds a task may sit in this state before the stall sweeper emits a
            `tasks.stalled` event (once per stall episode, re-armed on the next
            transition). Must be a positive integer when set. Omit or null to
            never stall. The event does not move the task — route on it with a
            webhook/trigger.
        on_enter:
          type: object
          nullable: true
          additionalProperties: true
    WorkflowTransition:
      type: object
      description: >-
        A named, directional move. `from` is a list of source states; `to` is
        one target state. `guard` is a JSON Logic expression over
        `{task, transition, principal}` that must be truthy for the move to
        apply.
      additionalProperties: true
      required:
        - name
        - from
        - to
      properties:
        name:
          type: string
        from:
          type: array
          items:
            type: string
        to:
          type: string
        guard:
          type: object
          nullable: true
        requires_approval:
          type: boolean
          description: >-
            Gate the transition behind a human approval. When `true`, firing the
            transition (by anyone other than the approval resolution itself)
            parks a pending `ApprovalItem` instead of moving the task; the task
            exposes `pending_transition` until the item resolves. Approval fires
            the transition as the `approval` principal (its guard re-evaluated at
            resolution time); rejection or expiry clears the gate and appends a
            history note.
    Workflow:
      type: object
      properties:
        id:
          type: string
        project_id:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        version:
          type: integer
          description: >
            Incremented on every write that changes the state machine; prior
            versions are archived. A task pins the version it entered on, so these
            fields are a draft for tasks created from now on rather than a live
            rewrite of the ones already in flight.
          example: 1
        states:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowState'
        transitions:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowTransition'
        payload_schema:
          type: object
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateWorkflowRequest:
      type: object
      required:
        - name
        - states
        - transitions
      properties:
        project_id:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        states:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowState'
        transitions:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowTransition'
        payload_schema:
          type: object
          nullable: true
        version_label:
          type: string
          description: >-
            Optional tag for the version this create archives, e.g. `initial`.
          example: initial
    UpdateWorkflowRequest:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        states:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowState'
        transitions:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowTransition'
        payload_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 definition field, since no version
            is archived.
          example: pre-rewire
    WorkflowVersion:
      type: object
      description: >-
        An immutable archive of a workflow's state machine at one version.
      properties:
        id:
          type: string
          description: Public ID of the archived version
          example: wfl_ver_V1StGXR8Z5jdHi6B
        workflow_id:
          x-soat-ref: workflows
          type: string
          description: Public ID of the workflow this version belongs to
          example: wfl_V1StGXR8Z5jdHi6B
        version:
          type: integer
          description: The archived version number
          example: 1
        config:
          type: object
          additionalProperties: true
          description: >-
            The workflow's versioned surface as it stood at this version:
            `states`, `transitions` and `payload_schema`. Name and description are
            metadata — bumping the version when one of them changes would make two
            version numbers denote the same state machine, which is exactly what a
            task cites.


            Deliberately open rather than a fixed schema: an archive written by an
            earlier release of SOAT reflects the workflow surface **of its own
            time**, so it may carry fields the current API no longer documents.
          properties:
            states:
              type: array
              items:
                $ref: '#/components/schemas/WorkflowState'
            transitions:
              type: array
              items:
                $ref: '#/components/schemas/WorkflowTransition'
            payload_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
    RestoreWorkflowVersionRequest:
      type: object
      properties:
        label:
          type: string
          description: >-
            Optional tag for the new version the restore archives. Defaults to
            `restored from vN`.
          example: rollback to pre-rewire
