openapi: 3.1.0
info:
  title: Milford HTTP API
  version: 0.0.1
  description: |
    Run flows, list them, and receive signed webhooks. The server is stateless and has no database.
    Generate a client for your language from this file with any OpenAPI generator.
servers:
  - url: http://localhost:8080
security:
  - bearer: []
paths:
  /health:
    get:
      operationId: health
      summary: Health check
      description: Needs no token. Use it for container and load balancer probes.
      security: []
      responses:
        "200":
          description: The server is up.
          content:
            application/json:
              schema:
                type: object
                required: [status]
                properties:
                  status: { type: string, const: ok }
  /v1/flows:
    get:
      operationId: listFlows
      summary: List flows
      responses:
        "200":
          description: The flows loaded at startup.
          content:
            application/json:
              schema:
                type: object
                required: [flows]
                properties:
                  flows:
                    type: array
                    items: { $ref: "#/components/schemas/FlowSummary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /v1/flows/{id}/run:
    post:
      operationId: runFlow
      summary: Run a flow
      description: |
        Runs the flow and returns its trace. The response is `200` even when a node failed, so check `ok`.

        Send `Accept: text/event-stream` to receive events while the run progresses. The event names are
        `node:start`, `node:done`, `node:error`, `node:skipped` and `provider:call`, and the last event is
        `result` with the same body as the JSON response. If the server is busy, the stream ends with a `result`
        event that carries an `error` instead of an HTTP `503`. Closing the connection cancels the run.

        Send an `Idempotency-Key` to make retries safe. A retry with the same key and input returns the first
        successful result with the header `Idempotent-Replayed: true` instead of running the flow again.
        The header is ignored for event streams.
      parameters:
        - name: id
          in: path
          required: true
          description: Flow id.
          schema: { type: string }
        - name: Idempotency-Key
          in: header
          required: false
          description: |
            Any string that is unique per logical request, for example an MQ message id. Results are kept
            for `server.idempotencyTtlMs` (default 10 minutes), per server instance, and are lost on restart.
            Failed runs are not kept, so a retry after a failure runs again.
          schema: { type: string }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                input:
                  type: object
                  description: Run input. Templates read it as `{{input.field}}`.
                  additionalProperties: true
            example:
              input:
                service: payments
                message: "Connection refused: db-primary:5432"
      responses:
        "200":
          description: The run finished. Check `ok` for node failures.
          headers:
            Idempotent-Replayed:
              description: Present with the value `true` when the result is a replay of an earlier request with the same key.
              schema: { type: string, const: "true" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RunResult" }
            text/event-stream:
              schema:
                type: string
                description: Server-sent events. See the operation description.
        "400":
          description: The body is not JSON, or `input` is not an object.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404":
          description: Unknown flow id.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "413": { $ref: "#/components/responses/TooLarge" }
        "422":
          description: The `Idempotency-Key` was already used with a different input.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "500":
          description: The engine could not run the flow.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "503":
          description: Too many runs are active. Retry after the delay in `Retry-After`.
          headers:
            Retry-After:
              schema: { type: string }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
  /hooks/{id}:
    post:
      operationId: webhook
      summary: Send a signed webhook
      description: |
        Runs the flow of a webhook channel. It does not use the bearer token. Sign the request instead:
        `x-milford-signature` is `sha256=` plus the hex HMAC-SHA256 of `<x-milford-timestamp>.<raw body>` with the
        channel secret. Timestamps more than five minutes from the server clock are rejected.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          description: Channel id.
          schema: { type: string }
        - name: x-milford-timestamp
          in: header
          required: true
          description: Current Unix time in seconds.
          schema: { type: string }
        - name: x-milford-signature
          in: header
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: The whole body becomes the flow input.
              additionalProperties: true
      responses:
        "200":
          description: The flow ran.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookResult" }
        "400":
          description: The body is not a JSON object.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401":
          description: Invalid or stale signature.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: Unknown webhook.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "413": { $ref: "#/components/responses/TooLarge" }
components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: One of `server.auth.tokens`. With no tokens configured the API is open.
  responses:
    Unauthorized:
      description: The bearer token is missing or wrong.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    TooLarge:
      description: The body is larger than `server.maxBodyBytes`.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }
    FlowSummary:
      type: object
      required: [id, nodes]
      properties:
        id: { type: string }
        nodes: { type: integer, description: Number of nodes in the flow. }
        description: { type: string, description: What the flow does. }
        input:
          type: object
          description: JSON Schema of the run input, when the flow declares one.
          additionalProperties: true
    NodeResult:
      type: object
      required: [success]
      properties:
        success: { type: boolean }
        output: { type: string, description: Text that downstream templates can use. }
        data:
          description: Structured value. For a `decision` node it holds `choice`, `confidence`, `probabilities` and more.
        error: { type: string }
    NodeState:
      type: object
      required: [status]
      properties:
        status: { type: string, enum: [done, error, skipped] }
        result: { $ref: "#/components/schemas/NodeResult" }
        ms: { type: number, description: Node duration in milliseconds. }
    RunResult:
      type: object
      required: [ok, runId, nodes]
      properties:
        ok: { type: boolean, description: False when any node failed. }
        runId: { type: string }
        nodes:
          type: object
          description: The trace, keyed by node id.
          additionalProperties: { $ref: "#/components/schemas/NodeState" }
        output:
          description: Result of the flow's `output` node, when one ran.
          $ref: "#/components/schemas/NodeResult"
    WebhookResult:
      type: object
      required: [ok, runId]
      properties:
        ok: { type: boolean }
        runId: { type: string }
        output: { type: string }
        data: {}
