# AUTO-GENERATED tier-A mirror spec — do not edit.
# Source: openapi/runtime/tasks.yaml (runtime 0.71.2)
#  + policy: openapi/mirror-policies/tasks.json
# Regenerate: node scripts/generate-mirror-specs.mjs
openapi: 3.0.3
info:
  title: naturali.ai — Tasks API
  version: 1.0.0
  description: >-
    Tasks: units of work that move through a workflow's states, carrying their own context and
    automation, with every transition guarded and recorded. A fully runtime-backed module — this
    spec is generated verbatim from the runtime's own, re-rooted under /v1/projects/{project_id}.
    The project in the path is authorized by naturali and enforced upstream by the project's scoped
    credential.


    This module mirrors the upstream runtime verbatim (tier A, #304): paths are the runtime's own
    re-rooted under /v1/projects/{project_id}, and every field, method, status code and error shape
    passes through unchanged. Errors raised by the runtime arrive in its envelope; errors raised by
    naturali itself (authentication, project resolution, an unreachable runtime) use naturali's.
  contact:
    name: naturali.ai
    url: https://naturali.ai
servers:
  - url: "{baseUrl}"
    description: Host of your naturali.ai deployment; every path carries the /v1 prefix.
    variables:
      baseUrl:
        description: Base host URL.
        default: https://api.naturali.ai
tags:
  - name: Tasks
    description: Manage tasks and their transitions
security:
  - bearerAuth: []
  - oauth2:
      - mcp:access
paths:
  /v1/projects/{project_id}/tasks:
    get:
      description: Lists tasks (the board query). Filter by workflow, state, status, automation status, or
        assignee — `GET /tasks?workflow_id=...&state=...` is one board column.
      tags:
        - Tasks
      summary: List tasks
      operationId: listTasks
      parameters:
        - name: workflow_id
          in: query
          required: false
          schema:
            type: string
        - name: state
          in: query
          required: false
          schema:
            type: string
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - open
              - closed
        - name: automation_status
          in: query
          required: false
          description: >-
            Filter by the current state's dispatch status. Repeat the parameter to OR values. `none`
            selects the tasks whose `automation_status` is `null` — the ones that never entered a
            state with an automation. It is a value a task really holds, so it is a value of the
            filter too; the parameter's own absence already means "every task".


            A value outside the enum, empty string included, is a `400`.
          schema:
            type: array
            items:
              type: string
              enum:
                - running
                - completed
                - failed
                - unrouted
                - paused
                - none
          style: form
          explode: true
          example:
            - running
        - name: assignee
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            minimum: 1
            maximum: 100
            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 tasks
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Task"
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
    post:
      description: "Creates a task bound to a workflow. By default the task is placed in the workflow's
        initial state; passing `state` places it directly in that named state instead — an alternate
        entry point for starting a task mid-flow (e.g. \"a new recorte for an existing theme by
        id\"), rather than re-submitting from the initial state and hoping a guard or similarity
        gate recognizes it. Entering the resulting state, initial or named, behaves identically:
        that state's `on_enter` automation fires and its `stalled_after` clock arms."
      tags:
        - Tasks
      summary: Create a task
      operationId: createTask
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTaskRequest"
      responses:
        "201":
          description: Task created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          description: Bad request — invalid payload (`TASK_PAYLOAD_INVALID`), or `state` does not name a
            declared state of the workflow (`TASK_STATE_NOT_FOUND`)
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
        "404":
          description: Workflow not found
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/tasks/{task_id}:
    get:
      description: Retrieves a task, including its active dispatch and automation status.
      tags:
        - Tasks
      summary: Get a task
      operationId: getTask
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Task details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
        "404":
          description: Task not found
    patch:
      description: "Updates a task's payload, title, or assignee. `state` is never directly writable —
        move it with a transition; sending a `state` field is rejected as an unknown field
        (`VALIDATION_FAILED`). `payload` is shallow-merged over the existing payload (PATCH
        semantics): keys the request omits are preserved. The payload is caller-owned; the
        automation result lives in the read-only `last_result` field, which no patch can reach. The
        merged payload is validated against the workflow's `payload_schema`."
      tags:
        - Tasks
      summary: Update a task
      operationId: updateTask
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTaskRequest"
      responses:
        "200":
          description: Task updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          description: Bad request (invalid payload)
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
        "404":
          description: Task not found
    delete:
      description: Deletes a task. Its transition history cascades.
      tags:
        - Tasks
      summary: Delete a task
      operationId: deleteTask
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Task deleted
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
        "404":
          description: Task not found
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/tasks/{task_id}/transitions:
    post:
      description: Fires a named transition on a task. The transition must exist in the workflow and be
        valid from the task's current state; its guard must pass. This is the single path every
        state change routes through. A transition declaring `requires_approval` does not move the
        task — it parks a pending ApprovalItem and returns the task with `pending_transition` set;
        the move applies only when the approval is approved.
      tags:
        - Tasks
      summary: Transition a task
      operationId: transitionTask
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TransitionTaskRequest"
      responses:
        "200":
          description: The task after the transition
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          description: The transition does not exist, is not valid, or its guard rejected the move
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
        "404":
          description: Task not found
        "409":
          description: A concurrent transition made this one invalid, or the task is closed
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/tasks/{task_id}/pause:
    post:
      description: >-
        Pauses a task's automation. While a pause is in force no state's `on_enter` dispatches and
        no retry chain continues — an agent generation, a tool call and a sub-orchestration alike —
        so the task stops spending without losing its place.

        A workflow has no run object, so this is the workflow half of pause-orchestration-run: the
        pause lands on the instance, which is the task. Transitions are deliberately still allowed —
        a move costs nothing while every dispatch it would start is suppressed — so a board stays
        usable under a pause. Entering a state whose dispatch is suppressed records
        `automation_status: paused`, which resume-task reads to know that state still owes its work.

        A dispatch already in flight is left to finish, and its outcome still routes; only what
        would start after it is suppressed.

        Idempotent: pausing an already-paused task answers with it unchanged.
      tags:
        - Tasks
      summary: Pause a task
      operationId: pauseTask
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PauseTaskRequest"
      responses:
        "200":
          description: The paused task
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
        "404":
          description: Task not found
        "409":
          description: The task is closed, so it has no automation left to pause
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/tasks/{task_id}/resume:
    post:
      description: >-
        Lifts a task's pause. When the pause suppressed the current state's `on_enter` —
        `automation_status: paused` — that dispatch is started now, as the caller resuming rather
        than as whoever last moved the task. A state whose dispatch had already completed, or that
        declares none, is left alone, so a resume never re-spends work the pause did not stop.

        This is the only way a pause is lifted; a task that is merely idle is advanced by firing a
        transition instead.
      tags:
        - Tasks
      summary: Resume a task
      operationId: resumeTask
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The resumed task
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
        "404":
          description: Task not found
        "409":
          description: The task carries no pause to lift
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/tasks/{task_id}/history:
    get:
      description: Returns the append-only transition history of a task.
      tags:
        - Tasks
      summary: Get task history
      operationId: getTaskHistory
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The task's transition history, oldest first
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TaskTransition"
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
        "404":
          description: Task not found
    parameters:
      - $ref: "#/components/parameters/ProjectId"
components:
  schemas:
    Task:
      type: object
      properties:
        id:
          type: string
        project_id:
          type: string
        workflow_id:
          type: string
        workflow_version:
          type: integer
          nullable: true
          description: The workflow version this task runs on, fixed when the task was created. Transitions,
            approval gates and payload validation all resolve through it, so editing the workflow
            never re-shapes a task already in flight. `null` for tasks created before pinning
            existed, which run on the live definition.
          example: 1
        title:
          type: string
        state:
          type: string
        status:
          type: string
          enum:
            - open
            - closed
        payload:
          type: object
          description: Caller-owned task data; input to guards (as `task.payload`) and dispatch mappings. The
            engine never writes into it except the workflow's declared `payload_writes`.
        metadata:
          description: The caller-owned key/value metadata supplied when the task was created, returned
            verbatim. Null when the task was created without any. Unlike `payload` it is invisible
            to guards and to `payload_writes`, so it is the place for an attribution label rather
            than task data.
          example:
            tenant_account_id: "42"
            source: zendesk
          allOf:
            - $ref: "#/components/schemas/NullableMetadataBag"
        last_result:
          description: Server-owned. The result of the current state's last completed dispatch, overwritten on
            every dispatch. Read-only — exposed to transition guards and
            `on_complete`/`payload_writes` expressions as `task.last_result`, a namespace a caller
            cannot write. An `agent` dispatch's result is the generation's output plus
            `ai_provider_id`, the provider that served its `model` (null when the route that ran
            named no serving target).
        assignee:
          type: string
          nullable: true
        active_dispatch:
          type: object
          nullable: true
          description: "{ kind, id, status } of the current state's dispatch, if any. `kind` is `generation`,
            `orchestration_run` or `tool_call`; a `tool_call` always carries a null `id`, since a
            direct tool call leaves no addressable record. Carries an additional `attempt` (1-based)
            while the state's `on_enter.retry` policy is in effect."
        automation_status:
          type: string
          nullable: true
          enum:
            - running
            - completed
            - failed
            - unrouted
            - paused
            - null
          description: Status of the current state's dispatch. `null` until a state with an automation is
            entered. `paused` means an operator pause suppressed this state's `on_enter` before it
            ran, so resume-task will start it.
        pause_requested_at:
          type: string
          format: date-time
          nullable: true
          description: When an operator paused this task's automation, or null when no pause is in force.
        pause_reason:
          type: string
          nullable: true
          description: The reason supplied with the pause, when one was.
        automation_chain_depth:
          type: integer
          description: Server-owned. How many machine-driven transitions have run back-to-back with no outside
            intervention — a dispatch outcome routed through `on_complete`/`on_failure`, or a
            `transition-task` call made by a dispatched run or agent with its run-as token. Any move
            by a person, a plain API key, or an approval resolution resets it to `0`. Once it would
            exceed the server's limit (`TASK_AUTOMATION_CHAIN_LIMIT`, default 50) the next such
            transition is refused with `TASK_AUTOMATION_CHAIN_LIMIT`, bounding a cycle composed
            across workflows and orchestrations.
        pending_transition:
          type: string
          nullable: true
          description: The name of a `requires_approval` transition parked awaiting a human decision. Non-null
            while an ApprovalItem gates the move; the task stays in its current state and no other
            transition may fire until the approval resolves.
        entered_state_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    TaskTransition:
      type: object
      properties:
        id:
          type: string
        task_id:
          type: string
        from_state:
          type: string
          nullable: true
        to_state:
          type: string
        transition:
          type: string
          nullable: true
        principal_kind:
          type: string
          description: >
            Who made the move. `user` and `api_key` are authenticated principals; `automation` (the
            engine acting on an `on_enter` dispatch outcome) and `approval` (an approval resolution)
            are system principals. Named `principal_*`, not `actor_*`: these ids never reference the
            Actors module.
          enum:
            - user
            - api_key
            - automation
            - approval
        principal_id:
          type: string
          nullable: true
          description: >
            Public id of the principal that made the move — the user (`user_...`), or for `api_key`
            auth the key's own id (`key_...`), distinguishing which key acted. Null for
            `automation`, which has no principal: the cause is carried by `generation_id` /
            `orchestration_run_id` / `tool_id`, one per dispatch kind — exactly one of which is set
            on an automation move.
        generation_id:
          type: string
          nullable: true
          description: Set when an `agent` dispatch's generation caused the move.
        orchestration_run_id:
          type: string
          nullable: true
          description: Set when an `orchestration` dispatch's run caused the move.
        tool_id:
          type: string
          nullable: true
          description: >
            Set when a `tool` dispatch caused the move. A tool call produces no addressable record
            of its own, so the tool it called is what records why the task moved.
        note:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    CreateTaskRequest:
      type: object
      required:
        - workflow_id
        - title
      additionalProperties: false
      properties:
        workflow_id:
          type: string
        title:
          type: string
        payload:
          type: object
        assignee:
          type: string
          nullable: true
        state:
          type: string
          description: Name of a declared workflow state to create the task in directly, instead of the
            workflow's `initial` state. Must name a state declared on the workflow, or the request
            is rejected with `TASK_STATE_NOT_FOUND` (400). Defaults to the `initial` state.
        tool_context:
          type: object
          additionalProperties:
            type: string
          description: >-
            Key-value pairs forwarded as `X-Naturali-Context-<key>` headers on every `http` and
            `mcp` tool call made by this task's automation dispatches — the agent generations a
            state's `on_enter` starts, and the agent nodes of any orchestration run it starts. The
            header name is `X-Naturali-Context-` plus the key verbatim; no character is re-cased.

            Creation is the task's first move, so this is the bag the entry state's `on_enter` runs
            with. Each transition may replace it (see `TransitionTaskRequest.tool_context`).

            The reserved identity keys (`session_id`, `actor_id`, `actor_external_id`) are stripped
            in any casing and re-derived server-side, so a task-dispatched generation cannot forge
            them. A key outside the HTTP header-name grammar is rejected with
            `INVALID_TOOL_CONTEXT_KEY` (400).

            Write-only: the stored bag is never returned by any task read, and it is cleared when
            the task reaches a terminal state.
        metadata:
          description: >-
            Caller-supplied key/value metadata attached to the task record for attribution — which
            of your own tenants the task belongs to, the ticket that raised it, the import batch
            that created it. Round-trips verbatim on every read of the task, the list included, and
            survives every transition (a transition supplies no metadata of its own).


            The bag is caller-owned and no key is reserved: everything the engine decides about a
            task (`state`, `status`, `workflow_version`, `last_result`, `active_dispatch`, the
            automation fields) is a field of its own and cannot be written from here.


            Prefer this over `payload` for anything that is not task data: `payload` is read by
            every guard as `task.payload` and may be written by the workflow's declared
            `payload_writes`, so a label parked there is neither invisible to the state machine nor
            safe from it. A non-object is rejected with `400 VALIDATION_FAILED` and no task is
            created.
          example:
            tenant_account_id: "42"
            source: zendesk
          allOf:
            - $ref: "#/components/schemas/MetadataBag"
    UpdateTaskRequest:
      type: object
      additionalProperties: false
      properties:
        title:
          type: string
        payload:
          type: object
          description: Partial payload, shallow-merged over the existing payload. Omitted keys are preserved;
            provided keys overwrite. The merged result must satisfy the workflow's payload_schema.
        assignee:
          type: string
          nullable: true
    PauseTaskRequest:
      type: object
      properties:
        reason:
          type: string
          maxLength: 256
          description: Why the task is being paused, surfaced on `pause_reason`.
      additionalProperties: false
    TransitionTaskRequest:
      type: object
      required:
        - transition
      additionalProperties: false
      properties:
        transition:
          type: string
        note:
          type: string
          nullable: true
        tool_context:
          type: object
          additionalProperties:
            type: string
          description: >-
            Caller context for the automation dispatches the task makes from here on, forwarded as
            `X-Naturali-Context-<key>` headers on their tool calls.

            Supplying it **replaces** the task's stored bag wholesale; omitting it keeps the current
            one, so the context follows whoever last moved the task and survives every move that
            does not speak about it — including an approval gate, a retry, and an automation hop.
            Send an empty object to clear it without closing the task.

            The reserved identity keys (`session_id`, `actor_id`, `actor_external_id`) are stripped
            in any casing and re-derived server-side. A key outside the HTTP header-name grammar is
            rejected with `INVALID_TOOL_CONTEXT_KEY` (400).

            Write-only: never returned by a task read, and cleared when the transition closes the
            task.
    NullableMetadataBag:
      type: object
      nullable: true
      description: A `MetadataBag` on a field where `null` is meaningful — a full-replacement update that
        clears the bag, or a record whose bag was never set.
      example:
        author: John
        revision: 2
    MetadataBag:
      type: object
      description: >-
        Caller-owned annotations on a resource, stored as the object they were written as: the types
        a value was written with are the types a read returns, so a filter can ask an ordering
        question about a number. Unlike other body fields, keys are stored and returned verbatim in
        the casing supplied — they are not converted between snake_case and camelCase.


        No key is reserved, and that is the point: every piece of state the platform owns lives in
        its own typed column, so nothing written here reaches platform state. The platform never
        reads the bag — it is not an IAM context, not a policy input and not part of a prompt —
        which is what separates it from a tag bag.
      example:
        author: John
        revision: 2
  parameters:
    ProjectId:
      name: project_id
      in: path
      required: true
      description: Project public ID (proj_ prefix).
      schema:
        type: string
        example: proj_V1StGXR8Z5jdHi6B
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: A naturali API key (nat_sk_…) or a session JWT.
    oauth2:
      type: oauth2
      description: "A connected app's OAuth access token, issued by this API's authorization server
        (discovery: /.well-known/oauth-authorization-server). Its one scope carries every operation,
        confined to the projects the user chose when approving the app."
      flows:
        authorizationCode:
          authorizationUrl: https://api.naturali.ai/authorize
          tokenUrl: https://api.naturali.ai/token
          refreshUrl: https://api.naturali.ai/token
          scopes:
            mcp:access: Every operation this API serves, on the projects the grant covers.
