openapi: 3.0.3
info:
  title: SOAT Deciders API
  version: 1.0.0
  description: >-
    API for deciders — versioned question sets answered against a caller's
    input — and the append-only decisions they produce. Questions, input and
    answers follow the shapes of OpenAI's Decisions API.
  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: Deciders
    description: Manage deciders and request decisions from them
security:
  - bearerAuth: []
paths:
  /api/v1/deciders:
    get:
      tags:
        - Deciders
      summary: List deciders
      description: Returns the deciders defined in a project, newest first.
      operationId: listDeciders
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - $ref: './pagination.yaml#/components/parameters/Limit'
        - $ref: './pagination.yaml#/components/parameters/Offset'
      responses:
        '200':
          description: List of deciders
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Decider'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
    post:
      tags:
        - Deciders
      summary: Create a decider
      description: >-
        Creates a decider at version 1: a named question set and the backend
        that answers it, exactly one of `agent_id` and `tool_id`. Every question
        declares a finite answer space, validated here. The backend must be in
        the decider's project. An agent must carry no tool surface — no tool
        binding left active by `active_tool_ids` and no
        `knowledge_config.write_memory_store_id` — or the create is refused with
        `400 DECIDER_AGENT_NOT_TOOL_LESS`. A tool must be `http` or `pipeline`
        and must not pin `input` or `questions` in its `preset_parameters`, or
        the create is refused with `400 DECIDER_TOOL_NOT_CALLABLE`. A duplicate
        `name` in the project is `409 NAME_CONFLICT`.
      operationId: createDecider
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDeciderRequest'
      responses:
        '201':
          description: Decider created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Decider'
        '400':
          description: Bad request — invalid questions, an unknown or tool-bearing agent
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '409':
          description: A decider with this name already exists in the project

  /api/v1/deciders/{decider_id}:
    get:
      tags:
        - Deciders
      summary: Get a decider
      description: Returns a decider with its current question set and version.
      operationId: getDecider
      x-soat-resource:
        kind: decider
        from: decider_id
      parameters:
        - $ref: '#/components/parameters/DeciderId'
      responses:
        '200':
          description: The decider
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Decider'
        '401':
          description: Unauthorized
        '404':
          description: Decider not found
    patch:
      tags:
        - Deciders
      summary: Update a decider
      description: >-
        Updates any of `name`, `description`, the backend and `questions`. Naming
        `agent_id` or `tool_id` replaces the current backend; naming both is
        `400`. Only a change to `questions` archives a new version; a rename, a
        new backend or a rewrite of the questions the decider already holds
        leaves `version` where it is. A new backend is held to the same rules as
        on create.
      operationId: updateDecider
      x-soat-resource:
        kind: decider
        from: decider_id
      parameters:
        - $ref: '#/components/parameters/DeciderId'
        - $ref: './concurrency.yaml#/components/parameters/IfMatchVersion'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDeciderRequest'
      responses:
        '200':
          description: Decider updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Decider'
        '400':
          description: Bad request — invalid questions, an unknown or tool-bearing agent
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Decider not found
        '409':
          $ref: './concurrency.yaml#/components/responses/VersionConflict'
    delete:
      tags:
        - Deciders
      summary: Delete a decider
      description: >-
        Deletes a decider and its version archive. Its decisions remain and keep
        naming the decider's ID.
      operationId: deleteDecider
      x-soat-resource:
        kind: decider
        from: decider_id
      parameters:
        - $ref: '#/components/parameters/DeciderId'
      responses:
        '204':
          description: Decider deleted
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Decider not found

  /api/v1/deciders/{decider_id}/versions:
    get:
      tags:
        - Deciders
      summary: List a decider's versions
      description: >-
        Returns the decider's archived question sets, newest first. A version is
        written on create and on every write that changes `questions`.
      operationId: listDeciderVersions
      x-soat-resource:
        kind: decider
        from: decider_id
      parameters:
        - $ref: '#/components/parameters/DeciderId'
        - $ref: './pagination.yaml#/components/parameters/Limit'
        - $ref: './pagination.yaml#/components/parameters/Offset'
      responses:
        '200':
          description: List of decider versions, newest first
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DeciderVersion'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '404':
          description: Decider not found

  /api/v1/deciders/{decider_id}/versions/{version}:
    get:
      tags:
        - Deciders
      summary: Fetch an archived decider version
      description: >-
        Returns the question set a version held. A decision names the
        `decider_version` it was answered under, so this is how its criteria
        are read after the decider has changed.
      operationId: getDeciderVersion
      x-soat-resource:
        kind: decider
        from: decider_id
      parameters:
        - $ref: '#/components/parameters/DeciderId'
        - $ref: '#/components/parameters/Version'
      responses:
        '200':
          description: Archived decider version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeciderVersion'
        '400':
          description: Bad request — version is not a positive integer
        '401':
          description: Unauthorized
        '404':
          description: Not found

  /api/v1/deciders/{decider_id}/versions/{version}/restore:
    post:
      tags:
        - Deciders
      summary: Restore an archived decider version
      description: >-
        Writes an archived question set back as the decider's live one, which
        archives it again as a new version rather than rewinding the counter.
        Restoring the question set the decider already holds is a no-op.
      operationId: restoreDeciderVersion
      x-soat-resource:
        kind: decider
        from: decider_id
      parameters:
        - $ref: '#/components/parameters/DeciderId'
        - $ref: '#/components/parameters/Version'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RestoreDeciderVersionRequest'
      responses:
        '200':
          description: The decider, at its new version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Decider'
        '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/deciders/{decider_id}/decisions:
    post:
      tags:
        - Deciders
      summary: Request a decision
      description: >-
        Evaluates the decider against `input` and records the decision. The
        questions come from the decider, never from the request, so a call site
        can only supply what is judged. To send the questions with the request,
        use `POST /api/v1/decisions`.


        Everything that can refuse the request is checked before the decision
        is written, so a refusal is a `4xx` and never a polled failure: the
        agent's tool surface (`400 DECIDER_AGENT_NOT_TOOL_LESS`), a tool that
        cannot answer (`400 DECIDER_TOOL_NOT_CALLABLE`), a paused project
        (`409 PROJECT_PAUSED`) and, for an agent, quota admission
        (`429 QUOTA_EXCEEDED`).


        A tool backend is called with `{ input, questions }` and must answer
        `{ answers: [{ name, probability | choice | score, probabilities?, confidence? }] }`,
        the request and response of OpenAI's Decisions API; an answer outside
        that contract fails the decision with `DECISION_ANSWER_INVALID`.


        With `wait: false`, the default, the answer is `201` with the decision
        `queued`; poll `GET /api/v1/decisions/{decision_id}` or subscribe to
        `decisions.completed` and `decisions.failed`. With `wait: true` it is
        `201` with the decision settled.
      operationId: createDecision
      x-soat-resource:
        kind: decider
        from: decider_id
      parameters:
        - $ref: '#/components/parameters/DeciderId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDecisionRequest'
      responses:
        '201':
          description: The decision — `queued`, or settled when `wait` is true
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Decision'
        '400':
          description: Bad request — a missing or malformed input, a non-object metadata bag, or a tool-bearing agent
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Decider not found
        '409':
          description: The project is paused
        '429':
          description: A generation quota is exhausted

  /api/v1/decisions:
    post:
      tags:
        - Deciders
      summary: Request a decision with inline questions
      description: >-
        Evaluates `questions` sent with the request against `input`, answered
        by the agent or tool the request names (exactly one of `agent_id` and
        `tool_id`, held to the same rules as a decider's backend), and records
        the decision with its questions. The request and answers are those of
        OpenAI's Decisions API, with the backend in place of `model`.


        No decider names the questions, so the decision carries them in
        `questions` and has no `decider_id` or `decider_version`. Admission,
        `wait` and the tool contract are those of
        `POST /api/v1/deciders/{decider_id}/decisions`.
      operationId: createInlineDecision
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateInlineDecisionRequest'
      responses:
        '201':
          description: The decision — `queued`, or settled when `wait` is true
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Decision'
        '400':
          description: Bad request — invalid questions or input, no backend or both, or a backend that cannot answer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '409':
          description: The project is paused
        '429':
          description: A generation quota is exhausted
    get:
      tags:
        - Deciders
      summary: List decisions
      description: Returns the decisions in a project, newest first.
      operationId: listDecisions
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: decider_id
          in: query
          description: Only decisions requested from this decider
          schema:
            type: string
            example: dcd_V1StGXR8Z5jdHi6B
        - name: status
          in: query
          required: false
          description: >-
            Only decisions in these statuses. Repeat the parameter to OR values.
            A value outside the enum, empty string included, is a `400`.
          schema:
            type: array
            items:
              type: string
              enum: [queued, running, completed, failed]
          style: form
          explode: true
        - $ref: './pagination.yaml#/components/parameters/Limit'
        - $ref: './pagination.yaml#/components/parameters/Offset'
      responses:
        '200':
          description: List of decisions
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Decision'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden

  /api/v1/decisions/{decision_id}:
    get:
      tags:
        - Deciders
      summary: Get a decision
      description: >-
        Returns a decision. Poll it after a `wait: false` request until
        `status` is `completed` or `failed`.
      operationId: getDecision
      x-soat-resource:
        kind: decision
        from: decision_id
      parameters:
        - name: decision_id
          in: path
          required: true
          description: The decision ID
          schema:
            type: string
            example: dec_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: The decision
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Decision'
        '401':
          description: Unauthorized
        '404':
          description: Decision not found
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  parameters:
    DeciderId:
      name: decider_id
      in: path
      required: true
      description: The decider ID
      schema:
        type: string
        example: dcd_V1StGXR8Z5jdHi6B
    Version:
      name: version
      in: path
      required: true
      description: The archived version number
      schema:
        type: integer
        minimum: 1
  schemas:
    DeciderChoice:
      type: object
      additionalProperties: false
      required:
        - value
        - description
      properties:
        value:
          type: string
          description: What a `choice` answer carries; unique within the question
          example: billing
        description:
          type: string
          example: Payments, invoices and refunds.
    DeciderLevel:
      type: object
      additionalProperties: false
      required:
        - label
        - description
      properties:
        label:
          type: string
          example: Workaround available
        description:
          type: string
          example: A task fails, but another way works.
    DeciderQuestion:
      type: object
      additionalProperties: false
      required:
        - type
        - name
        - instructions
      description: >-
        One question and its answer space. A `predicate` checks a condition and
        is answered with the probability it holds. A `choice` selects one of
        `choices` (2 to 20, in the order the model is shown them). A `score`
        rates against ordered `levels` (2 to 20); its answer is a level index,
        or a value between two when a tool weighs them.
      properties:
        type:
          type: string
          enum: [predicate, choice, score]
          example: choice
        name:
          type: string
          description: >-
            Names the question's answer. Unique within the set; starts with a
            letter or underscore and holds only letters, digits and
            underscores (at most 64).
          example: department
        instructions:
          type: string
          description: What to judge
          example: Which department should handle this complaint?
        choices:
          type: array
          description: A `choice` question's options; required for `choice` only
          items:
            $ref: '#/components/schemas/DeciderChoice'
        levels:
          type: array
          description: A `score` question's ordered levels; required for `score` only
          items:
            $ref: '#/components/schemas/DeciderLevel'
    DeciderQuestions:
      type: array
      minItems: 1
      maxItems: 20
      description: The questions, in the order the model is shown them.
      items:
        $ref: '#/components/schemas/DeciderQuestion'
      example:
        - type: choice
          name: department
          instructions: Which department should handle this complaint?
          choices:
            - value: billing
              description: Payments, invoices and refunds.
            - value: technical
              description: Problems using the product.
        - type: predicate
          name: needs_human
          instructions: Must a person read this before any automated reply?
    DecisionInput:
      description: >-
        What is judged, not stored: a string; a list of `user` messages whose
        `content` is a string or `input_text` and `input_image` parts (an image
        as a base64 data URL); or any other JSON value, which an agent is shown
        as indented JSON. An array of objects carrying `role` is read as
        messages.
      example: I was charged twice for the same order.
    Decider:
      type: object
      properties:
        id:
          type: string
          example: dcd_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          example: proj_V1StGXR8Z5jdHi6B
        name:
          type: string
          example: support-triage
        description:
          type: string
          nullable: true
        agent_id:
          x-soat-ref: agents
          type: string
          nullable: true
          description: The tool-less agent that answers; null when a tool does
          example: agent_V1StGXR8Z5jdHi6B
        tool_id:
          x-soat-ref: tools
          type: string
          nullable: true
          description: The http or pipeline tool that answers; null when an agent does
          example: tool_V1StGXR8Z5jdHi6B
        version:
          type: integer
          description: >-
            The question set's version. Bumped only by a write that changes
            `questions`.
          example: 1
        questions:
          $ref: '#/components/schemas/DeciderQuestions'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateDeciderRequest:
      type: object
      additionalProperties: false
      description: Names exactly one of `agent_id` and `tool_id`.
      required:
        - name
        - questions
      properties:
        project_id:
          x-soat-ref: projects
          type: string
          description: Project ID (required if not using project key auth)
          example: proj_V1StGXR8Z5jdHi6B
        name:
          type: string
          description: Human-readable name, unique per project
          example: support-triage
        description:
          type: string
          nullable: true
        agent_id:
          x-soat-ref: agents
          type: string
          description: A tool-less agent that answers
          example: agent_V1StGXR8Z5jdHi6B
        tool_id:
          x-soat-ref: tools
          type: string
          description: An http or pipeline tool that answers
          example: tool_V1StGXR8Z5jdHi6B
        questions:
          $ref: '#/components/schemas/DeciderQuestions'
        version_label:
          type: string
          description: Optional tag for version 1, e.g. `initial`
          example: initial
    UpdateDeciderRequest:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        agent_id:
          x-soat-ref: agents
          type: string
          description: Replaces the backend with this agent
        tool_id:
          x-soat-ref: tools
          type: string
          description: Replaces the backend with this tool
        questions:
          $ref: '#/components/schemas/DeciderQuestions'
        version_label:
          type: string
          description: >-
            Optional tag for the version this write archives. Ignored when the
            write changes no question, since no version is archived.
          example: add-account-option
        expected_version:
          description: Refuses the write unless the decider is at this version.
          allOf:
            - $ref: './concurrency.yaml#/components/schemas/ExpectedVersion'
    DeciderVersion:
      type: object
      description: An immutable archive of a decider's question set at one version.
      properties:
        id:
          type: string
          example: dcd_ver_V1StGXR8Z5jdHi6B
        decider_id:
          x-soat-ref: deciders
          type: string
          example: dcd_V1StGXR8Z5jdHi6B
        version:
          type: integer
          example: 1
        config:
          type: object
          additionalProperties: true
          description: The versioned surface as it stood at this version.
          properties:
            questions:
              $ref: '#/components/schemas/DeciderQuestions'
        label:
          type: string
          nullable: true
          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
    RestoreDeciderVersionRequest:
      type: object
      additionalProperties: false
      properties:
        label:
          type: string
          description: Optional tag for the new version. Defaults to `restored from vN`.
          example: rollback
    CreateDecisionRequest:
      type: object
      additionalProperties: false
      required:
        - input
      properties:
        input:
          $ref: '#/components/schemas/DecisionInput'
        metadata:
          description: >-
            Caller-owned annotations stored on the decision and returned on
            every read — typically the id of what was judged, since the input
            itself is not stored. Written once, with the decision. A non-object
            is rejected with `400 VALIDATION_FAILED` and no decision is written.
          example:
            ticket_id: ZD-48213
          allOf:
            - $ref: './metadata.yaml#/components/schemas/MetadataBag'
        wait:
          type: boolean
          default: false
          # Pinned to `true` for tool calls: a tool caller wants the answer,
          # and an orchestration node reading a queued decision finds none.
          x-soat-tool-forced: true
          description: >-
            True evaluates before answering and returns the settled decision.
            False — the default — returns the `queued` decision at once. A tool
            call, `builtin` or MCP, always waits.
          example: true
    CreateInlineDecisionRequest:
      type: object
      additionalProperties: false
      description: Names exactly one of `agent_id` and `tool_id`.
      required:
        - questions
        - input
      properties:
        project_id:
          x-soat-ref: projects
          type: string
          description: Project ID (required if not using project key auth)
          example: proj_V1StGXR8Z5jdHi6B
        agent_id:
          x-soat-ref: agents
          type: string
          description: A tool-less agent that answers
          example: agent_V1StGXR8Z5jdHi6B
        tool_id:
          x-soat-ref: tools
          type: string
          description: An http or pipeline tool that answers
          example: tool_V1StGXR8Z5jdHi6B
        questions:
          $ref: '#/components/schemas/DeciderQuestions'
        input:
          $ref: '#/components/schemas/DecisionInput'
        metadata:
          description: >-
            Caller-owned annotations stored on the decision. A non-object is
            rejected with `400 VALIDATION_FAILED`.
          example:
            ticket_id: ZD-48213
          allOf:
            - $ref: './metadata.yaml#/components/schemas/MetadataBag'
        wait:
          type: boolean
          default: false
          # Pinned to `true` for tool calls, as on a decider's decision.
          x-soat-tool-forced: true
          description: >-
            True evaluates before answering and returns the settled decision.
            False — the default — returns the `queued` decision at once. A tool
            call, `builtin` or MCP, always waits.
          example: true
    AnswerProbability:
      type: object
      additionalProperties: false
      properties:
        value:
          description: The choice's `value`, or the level's index
          oneOf:
            - type: string
            - type: integer
          example: billing
        label:
          type: string
          description: The level's `label`; on a `score` answer only
        probability:
          type: number
          minimum: 0
          maximum: 1
          example: 0.95
    DecisionAnswer:
      type: object
      description: >-
        One question's answer, named after it. A `predicate` carries the
        `probability` its condition holds — 1 or 0 from an agent, which states
        no calibrated probability. A `choice` carries the chosen `choice`. A
        `score` carries the level index, or from a tool a probability-weighted
        value between levels. `probabilities` and `confidence` are present only
        when a tool backend supplied them, carried as supplied; SOAT does not
        vouch for their meaning.
      properties:
        type:
          type: string
          enum: [predicate, choice, score]
        name:
          type: string
          example: department
        probability:
          type: number
          minimum: 0
          maximum: 1
          example: 0.92
        choice:
          type: string
          example: billing
        score:
          type: number
          example: 1.1
        probabilities:
          type: array
          items:
            $ref: '#/components/schemas/AnswerProbability'
        confidence:
          type: number
          minimum: 0
          maximum: 1
          example: 0.93
    Decision:
      type: object
      properties:
        id:
          type: string
          example: dec_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          example: proj_V1StGXR8Z5jdHi6B
        decider_id:
          x-soat-ref: deciders
          type: string
          nullable: true
          description: >-
            The decider asked; kept after the decider is deleted. Null for a
            decision sent with its own questions.
          example: dcd_V1StGXR8Z5jdHi6B
        decider_version:
          type: integer
          nullable: true
          description: The question-set version the decision was answered under
          example: 1
        questions:
          type: array
          nullable: true
          description: >-
            The questions a decision sent with its own was answered under. Null
            for a decider's decision, whose questions are its
            `decider_version`.
          items:
            $ref: '#/components/schemas/DeciderQuestion'
        status:
          type: string
          enum: [queued, running, completed, failed]
        answers:
          type: array
          nullable: true
          description: >-
            One answer per question, in question order. Null until the decision
            completes, then never rewritten.
          items:
            $ref: '#/components/schemas/DecisionAnswer'
        answers_by_name:
          type: object
          nullable: true
          description: >-
            The same answers keyed by question name, so a JSON Logic path reads
            one by name (`answers_by_name.department.choice`) rather than by
            position.
          additionalProperties:
            $ref: '#/components/schemas/DecisionAnswer'
        error:
          allOf:
            - $ref: './errors.yaml#/components/schemas/StoredError'
          nullable: true
          description: Why a `failed` decision failed.
        generation_id:
          x-soat-ref: generations
          type: string
          nullable: true
          description: >-
            The generation that answered, once one has; null for a tool
            backend. Its receipt carries what the decision cost.
          example: gen_V1StGXR8Z5jdHi6B
        metadata:
          allOf:
            - $ref: './metadata.yaml#/components/schemas/MetadataBag'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
