openapi: 3.1.0
info:
  title: JustFlows Workspace API
  version: 1.0.0
  description: >
    Operate published workflows, workflow runs, and human approvals using
    workspace-scoped service tokens.
servers:
  - url: /api/v1
    description: Current JustFlows installation
security:
  - workspaceToken: []
tags:
  - name: Workflows
  - name: Runs
  - name: Approvals
paths:
  /workflows:
    get:
      operationId: listWorkflows
      summary: List published workflows
      tags: [Workflows]
      x-required-ability: workflows:read
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Published workflows in the token workspace.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                type: object
                required: [data, meta, request_id]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Workflow'
                  meta:
                    $ref: '#/components/schemas/CursorMeta'
                  request_id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ApiDisabled'
  /workflows/{workflow_id}:
    parameters:
      - $ref: '#/components/parameters/WorkflowId'
    get:
      operationId: getWorkflow
      summary: Get a published workflow
      tags: [Workflows]
      x-required-ability: workflows:read
      responses:
        '200':
          description: The published workflow.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ApiDisabled'
  /workflows/{workflow_id}/runs:
    parameters:
      - $ref: '#/components/parameters/WorkflowId'
    post:
      operationId: startWorkflowRun
      summary: Start the published workflow version
      tags: [Runs]
      x-required-ability: runs:create
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: >
            Optional unique key for this logical attempt. Supplying the same
            key and request returns the original run for 24 hours. When omitted,
            the request starts normally without protection against duplicate
            runs caused by caller retries.
          schema:
            type: string
            minLength: 8
            maxLength: 200
            pattern: '^[A-Za-z0-9._:-]+$'
        - $ref: '#/components/parameters/RequestId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                payload:
                  type: object
                  additionalProperties: true
                  default: {}
            example:
              payload:
                order_id: 1842
                customer_email: customer@example.com
      responses:
        '202':
          description: >
            The run was accepted. On a replay the Idempotent-Replayed response
            header is true.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
            Idempotent-Replayed:
              description: Present and true when the original run is returned.
              schema:
                type: string
                const: 'true'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunReferenceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ApiDisabled'
  /runs:
    get:
      operationId: listRuns
      summary: List workflow runs
      tags: [Runs]
      x-required-ability: runs:read
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/PerPage'
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/RunStatus'
        - name: workflow_id
          in: query
          schema:
            type: string
            pattern: '^wf_[0-9A-HJKMNP-TV-Z]{26}$'
      responses:
        '200':
          description: Runs in the token workspace.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                type: object
                required: [data, meta, request_id]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Run'
                  meta:
                    $ref: '#/components/schemas/CursorMeta'
                  request_id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ApiDisabled'
  /runs/{run_id}:
    parameters:
      - $ref: '#/components/parameters/RunId'
    get:
      operationId: getRun
      summary: Get a workflow run
      tags: [Runs]
      x-required-ability: runs:read
      responses:
        '200':
          description: Safe run status, output, and failure information.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ApiDisabled'
  /runs/{run_id}/cancel:
    parameters:
      - $ref: '#/components/parameters/RunId'
    post:
      operationId: cancelRun
      summary: Cancel an active workflow run
      tags: [Runs]
      x-required-ability: runs:cancel
      responses:
        '200':
          description: The canceled run.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunReferenceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ApiDisabled'
  /approvals:
    get:
      operationId: listApprovals
      summary: List approval requests
      tags: [Approvals]
      x-required-ability: approvals:read
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/PerPage'
        - name: status
          in: query
          description: Defaults to pending.
          schema:
            type: string
            default: pending
            enum:
              - all
              - pending
              - approved
              - rejected
              - changes_requested
              - canceled
      responses:
        '200':
          description: Approval requests in the token workspace.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                type: object
                required: [data, meta, request_id]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Approval'
                  meta:
                    $ref: '#/components/schemas/CursorMeta'
                  request_id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ApiDisabled'
  /approvals/{approval_id}:
    parameters:
      - $ref: '#/components/parameters/ApprovalId'
    get:
      operationId: getApproval
      summary: Get an approval request
      tags: [Approvals]
      x-required-ability: approvals:read
      responses:
        '200':
          description: The approval request.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ApiDisabled'
  /approvals/{approval_id}/decision:
    parameters:
      - $ref: '#/components/parameters/ApprovalId'
    post:
      operationId: decideApproval
      summary: Resolve an approval request
      tags: [Approvals]
      x-required-ability: approvals:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [decision]
              properties:
                decision:
                  type: string
                  enum: [approved, rejected, changes_requested]
                feedback:
                  type:
                    - string
                    - 'null'
                  maxLength: 4000
              allOf:
                - if:
                    properties:
                      decision:
                        const: changes_requested
                  then:
                    required: [feedback]
                    properties:
                      feedback:
                        type: string
                        minLength: 1
            example:
              decision: changes_requested
              feedback: Use the approved customer-facing wording.
      responses:
        '200':
          description: The resolved approval request.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalDecisionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ApiDisabled'
components:
  securitySchemes:
    workspaceToken:
      type: http
      scheme: bearer
      bearerFormat: 'wst_v1_<ULID>.<secret>'
      description: A workspace service token shown once when created or rotated.
  headers:
    RequestId:
      description: Correlation ID supplied by the client or generated by the API.
      schema:
        type: string
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
        minimum: 1
  parameters:
    Cursor:
      name: cursor
      in: query
      description: Opaque cursor returned by the preceding list response.
      schema:
        type: string
    PerPage:
      name: per_page
      in: query
      description: Number of records to return.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    RequestId:
      name: X-Request-Id
      in: header
      required: false
      schema:
        type: string
        maxLength: 100
        pattern: '^[A-Za-z0-9._:-]+$'
    WorkflowId:
      name: workflow_id
      in: path
      required: true
      schema:
        type: string
        pattern: '^wf_[0-9A-HJKMNP-TV-Z]{26}$'
    RunId:
      name: run_id
      in: path
      required: true
      schema:
        type: string
        pattern: '^run_[0-9A-HJKMNP-TV-Z]{26}$'
    ApprovalId:
      name: approval_id
      in: path
      required: true
      schema:
        type: string
        pattern: '^apr_[0-9A-HJKMNP-TV-Z]{26}$'
  responses:
    Unauthorized:
      description: The Bearer token is missing, malformed, expired, or revoked.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
        WWW-Authenticate:
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: The token lacks the required ability.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: The resource was not found in the token workspace.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: The operation conflicts with current state or idempotency history.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unprocessable:
      description: The request is invalid or the workspace cannot perform the operation.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimited:
      description: A per-token rate limit was exceeded.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ApiDisabled:
      description: The site administrator has disabled the Workspace API.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    WorkflowResponse:
      type: object
      required: [data, request_id]
      properties:
        data:
          $ref: '#/components/schemas/Workflow'
        request_id:
          type: string
    RunResponse:
      type: object
      required: [data, request_id]
      properties:
        data:
          $ref: '#/components/schemas/Run'
        request_id:
          type: string
    RunReferenceResponse:
      type: object
      required: [data, request_id]
      properties:
        data:
          $ref: '#/components/schemas/RunReference'
        request_id:
          type: string
    ApprovalResponse:
      type: object
      required: [data, request_id]
      properties:
        data:
          $ref: '#/components/schemas/Approval'
        request_id:
          type: string
    ApprovalDecisionResponse:
      type: object
      required: [data, request_id]
      properties:
        data:
          $ref: '#/components/schemas/ApprovalDecision'
        request_id:
          type: string
    CursorMeta:
      type: object
      required: [next_cursor, previous_cursor, has_more]
      properties:
        next_cursor:
          type:
            - string
            - 'null'
        previous_cursor:
          type:
            - string
            - 'null'
        has_more:
          type: boolean
    Workflow:
      type: object
      required:
        - id
        - object
        - name
        - status
        - published_version
        - input
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: '^wf_[0-9A-HJKMNP-TV-Z]{26}$'
        object:
          type: string
          const: workflow
        name:
          type: string
        slug:
          type:
            - string
            - 'null'
        description:
          type:
            - string
            - 'null'
        status:
          type: string
          const: published
        published_version:
          type: object
          required: [number, published_at]
          properties:
            number:
              type: integer
            published_at:
              type:
                - string
                - 'null'
              format: date-time
        input:
          type: object
          required: [schema, example]
          properties:
            schema:
              type: object
              additionalProperties: true
            example:
              type:
                - object
                - 'null'
              additionalProperties: true
        created_at:
          type:
            - string
            - 'null'
          format: date-time
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
    RunStatus:
      type: string
      enum: [pending, running, waiting, finished, failed, canceled]
    Run:
      type: object
      required:
        - id
        - object
        - workflow_id
        - status
        - output
        - error
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: '^run_[0-9A-HJKMNP-TV-Z]{26}$'
        object:
          type: string
          const: run
        workflow_id:
          type:
            - string
            - 'null'
        status:
          $ref: '#/components/schemas/RunStatus'
        current_node_key:
          type:
            - string
            - 'null'
        output:
          description: Redacted workflow output.
        error:
          oneOf:
            - type: 'null'
            - type: object
              required: [code, message]
              properties:
                code:
                  type: string
                  const: workflow_run_failed
                message:
                  type: string
        execution_ms:
          type:
            - integer
            - 'null'
        started_at:
          type:
            - string
            - 'null'
          format: date-time
        finished_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type:
            - string
            - 'null'
          format: date-time
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
    RunReference:
      type: object
      required: [id, object, workflow_id, status, created_at, updated_at]
      properties:
        id:
          type: string
          pattern: '^run_[0-9A-HJKMNP-TV-Z]{26}$'
        object:
          type: string
          const: run
        workflow_id:
          type:
            - string
            - 'null'
        status:
          $ref: '#/components/schemas/RunStatus'
        started_at:
          type:
            - string
            - 'null'
          format: date-time
        finished_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type:
            - string
            - 'null'
          format: date-time
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
    Approval:
      type: object
      required:
        - id
        - object
        - run_id
        - workflow_id
        - status
        - decision
        - title
        - summary
        - request
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: '^apr_[0-9A-HJKMNP-TV-Z]{26}$'
        object:
          type: string
          const: approval
        run_id:
          type:
            - string
            - 'null'
        workflow_id:
          type:
            - string
            - 'null'
        status:
          type: string
          enum: [pending, approved, rejected, changes_requested, canceled]
        decision:
          type:
            - string
            - 'null'
        title:
          type:
            - string
            - 'null'
        summary:
          type:
            - string
            - 'null'
        request:
          type: object
          additionalProperties: true
          description: Redacted approval context safe for API presentation.
        due_at:
          type:
            - string
            - 'null'
          format: date-time
        responded_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type:
            - string
            - 'null'
          format: date-time
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
    ApprovalDecision:
      type: object
      required: [id, object, run_id, status, decision, responded_at, updated_at]
      properties:
        id:
          type: string
          pattern: '^apr_[0-9A-HJKMNP-TV-Z]{26}$'
        object:
          type: string
          const: approval
        run_id:
          type:
            - string
            - 'null'
        status:
          type: string
          enum: [approved, rejected, changes_requested]
        decision:
          type: string
          enum: [approved, rejected, changes_requested]
        responded_at:
          type:
            - string
            - 'null'
          format: date-time
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
    ErrorResponse:
      type: object
      required: [error, request_id]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
            message:
              type: string
            details:
              type: object
              additionalProperties: true
        request_id:
          type: string
