openapi: 3.0.3
info:
  title: Orkidata ETL Platform API
  version: 1.0.0
  contact:
    name: Orkidata Support
    email: support@orkidata.com
  license:
    name: Proprietary
  description: 'Build, schedule, and execute ETL workflows entirely through HTTP.


    Everything you can do programmatically with your Orkidata account is here:

    create workflows, define their step graph, ingest data, run synchronously

    or asynchronously, browse execution history, download exports, manage

    email recipients, inspect usage, and read your subscription state.


    ## Authentication


    Mint a Bearer token from **Profile → API tokens** in the web UI, then

    include it on every request:


    ```

    Authorization: Bearer <your token>

    ```


    Tokens can be listed and revoked via `/api/auth/token`. The web UI uses a

    session cookie internally, but session-cookie flows (login, register,

    password reset, profile edits) are not part of this public API — they

    live in the UI only.


    ## Rate limits per tier


    | Tier  | RPM | Exec/hr | Concurrent | Max data | Max exec sec | Emails/day |

    |-------|-----|---------|------------|----------|--------------|------------|

    | free  |  30 |  10     |  1         |  10 MB   |  60          |  0         |

    | entry | 120 |  60     |  5         |  500 MB  |  300         |  500       |

    | pro   | 600 |  300    |  20        |  5 GB    |  870         |  5,000     |


    ## Error response shape


    Every non-2xx response is JSON with at minimum an `error` field:


    ```json

    { "error": "Human-readable description", "retry_after": 60 }

    ```


    | Status | Meaning                                                                  |

    |--------|--------------------------------------------------------------------------|

    | 400    | Validation error in the request body / query                            |

    | 401    | Missing / invalid / expired Bearer token                                |

    | 403    | Authenticated, but not authorized for this resource                     |

    | 404    | Resource doesn''t exist (or you can''t see it — same code on purpose)     |

    | 409    | Conflict — usually a re-add of a previously unsubscribed/bounced entity |

    | 429    | Rate limit hit — `retry_after` indicates seconds until next attempt     |

    | 500    | Unhandled server error                                                  |


    ## Tutorial


    A complete walkthrough — list workflows, create one, save its definition,

    run it, inspect history, download the export — lives on the docs page at

    `https://orkidata.com/docs.html`.

    '
servers:
- url: https://orkidata.com
  description: Production
- url: https://dev.orkidata.com
  description: Dev / staging
security:
- bearerAuth: []
tags:
- name: Authentication & Account
  description: Sign up, log in, manage your account, mint API tokens.
- name: Workflows
  description: The workflow tree (folders + workflow entries) per user.
- name: Workflow Definition
  description: The step graph for a specific workflow.
- name: Execution
  description: Run a workflow synchronously or dispatch it asynchronously, auto-routed based on workload
    size.
- name: Session Runs
  description: Long-lived runs that collect data from multiple sources before triggering execution.
- name: Execution History
  description: Past runs, their outputs, and on-demand export downloads.
- name: Dashboards
  description: User-defined dashboard layouts and widgets.
- name: Widget Data
  description: Pre-aggregated data feeds for dashboard widgets.
- name: Data Storage
  description: User-uploaded files (CSV/JSON/Parquet/Excel/etc.) backed by S3.
- name: Connections
  description: Database (PostgreSQL/MySQL/SFTP/etc.) connection credentials.
- name: Email Recipients
  description: Verified-recipients list for the workflow `send_email` step, with custom tags.
- name: Billing
  description: Subscription, checkout, customer portal, add-on management (Stripe-backed).
- name: Usage
  description: Per-user usage metering (executions, RPM consumption, data volume).
paths:
  /api/auth/token:
    post:
      tags:
      - Authentication & Account
      summary: Mint an API Bearer token
      description: 'Creates a long-lived API token for programmatic access. The

        plaintext token is shown ONCE in the response — store it

        somewhere safe; it cannot be retrieved later. Re-minting

        automatically revokes any prior token.

        '
      responses:
        '200':
          description: Token created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiTokenResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    delete:
      tags:
      - Authentication & Account
      summary: Revoke the current API token
      responses:
        '200':
          description: Token revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/workflows:
    get:
      tags:
      - Workflows
      summary: Get the entire workflow tree
      description: 'Returns the user''s workflow tree (folders + workflow entries). The

        tree is hierarchical — folders may contain folders + workflows.

        '
      responses:
        '200':
          description: Workflow tree
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowList'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
      - Workflows
      summary: Replace the entire workflow tree
      description: '**Warning**: this replaces the WHOLE tree, not just the items you

        send. Always GET the tree first, mutate, then POST it back. Sending

        an incomplete tree will delete missing items.


        Empty-tree safety: posting `{ "items": [] }` when the stored tree

        is non-empty returns 400. Add `confirmEmpty: true` to acknowledge

        an intentional wipe.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WorkflowList'
      responses:
        '200':
          description: Tree saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/workflows/{workflowId}:
    get:
      tags:
      - Workflows
      summary: Get a single workflow's metadata
      parameters:
      - $ref: '#/components/parameters/workflowId'
      responses:
        '200':
          description: Workflow item
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowItem'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - Workflows
      summary: Update a single workflow's metadata fields
      parameters:
      - $ref: '#/components/parameters/workflowId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Fields to update (name, description, etc.)
              additionalProperties: true
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowItem'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/workflows/cleanup:
    post:
      tags:
      - Workflows
      summary: Bulk-delete workflows (scoped sweep)
      description: 'Deletes workflows by id. Used by the UI''s "delete folder" cascade

        and by E2E test cleanup. Each id is access-checked individually

        (you must own or have editor permission on each).

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - workflowIds
              properties:
                workflowIds:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: Cleanup completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /api/workflows/copy:
    post:
      tags:
      - Workflows
      summary: Duplicate a workflow (with definition + run-config)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - sourceWorkflowId
              properties:
                sourceWorkflowId:
                  type: string
                targetParentId:
                  type: string
                  description: Folder id to drop the copy into
                newName:
                  type: string
      responses:
        '200':
          description: Copy created
          content:
            application/json:
              schema:
                type: object
                properties:
                  newWorkflowId:
                    type: string
  /api/workflow/{workflowId}/definition:
    get:
      tags:
      - Workflow Definition
      summary: Get the step graph for a workflow
      parameters:
      - $ref: '#/components/parameters/workflowId'
      responses:
        '200':
          description: Step definition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowDefinition'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      tags:
      - Workflow Definition
      summary: Save the step graph for a workflow
      description: 'Replaces the entire step list. Step types and config schemas are

        documented in detail in `docs/API.md` § Step Types Reference.

        '
      parameters:
      - $ref: '#/components/parameters/workflowId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WorkflowDefinition'
      responses:
        '200':
          description: Saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
  /api/workflow/{workflowId}/execute:
    post:
      tags:
      - Execution
      summary: Execute a workflow (sync or async, auto-routed by workload size)
      description: "Auto-routes between sync and async execution based on workload\nsize:\n\n- **Small\
        \ workloads** run inline and return the full\n  `ExecutionResult` in the HTTP response.\n- **Larger\
        \ workloads** return `{ async: true, runId: ... }`\n  immediately. Poll `GET /api/execution/{runId}/status`\
        \ until\n  the run reaches a terminal status, then fetch the full\n  result via `/api/workflow/{id}/history/run/{runId}`.\n\
        \nExecution and concurrent-run rate limits apply (`exec_per_hour`,\n`concurrent`). Free tier is\
        \ also blocked from `webhook` /\n`send_email` step types.\n"
      parameters:
      - $ref: '#/components/parameters/workflowId'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                input:
                  type: object
                  description: Input parameters available to step config via `${input.foo}` interpolation
                  additionalProperties: true
                source:
                  type: string
                  enum:
                  - api
                  - ui
                  - scheduled
                  - file_watcher
                  - session
                  default: api
      responses:
        '200':
          description: Sync execution result OR async dispatch acknowledgement (depending on workflow
            size)
          content:
            application/json:
              schema:
                oneOf:
                - $ref: '#/components/schemas/ExecutionResult'
                - $ref: '#/components/schemas/AsyncDispatchResponse'
        '402':
          description: Daily / hourly execution cap exceeded
        '403':
          description: Tier-gate failed (e.g. free tier hitting webhook step)
        '429':
          $ref: '#/components/responses/RateLimited'
  /api/workflow/{workflowId}/scan-files:
    post:
      tags:
      - Execution
      summary: Scan watched-folder data-storage paths for matching files
      description: 'Returns the list of data-storage files that match the workflow''s

        configured `load_data` source patterns. Used by the file-watcher

        UI for preview before triggering an execution.

        '
      parameters:
      - $ref: '#/components/parameters/workflowId'
      responses:
        '200':
          description: Matched files
          content:
            application/json:
              schema:
                type: object
                properties:
                  files:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
  /api/execution/{runId}/status:
    get:
      tags:
      - Execution
      summary: Poll an async run's status
      description: 'Returns the current step, progress percentage, status (queued /

        running / completed / failed / timeout / cancelled), and a

        `result_summary` on completion. Runs that exceed the maximum

        execution window without progressing are auto-marked `failed`.

        '
      parameters:
      - $ref: '#/components/parameters/runId'
      responses:
        '200':
          description: Run status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AsyncRunStatus'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/execution/{runId}/cancel:
    post:
      tags:
      - Execution
      summary: Cancel an in-flight async run
      description: 'Records a cancellation request. The run aborts cooperatively at

        the next step boundary, so it may take up to one step''s duration

        to actually stop.

        '
      parameters:
      - $ref: '#/components/parameters/runId'
      responses:
        '200':
          description: Cancel signal sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/workflow/{workflowId}/runs:
    get:
      tags:
      - Session Runs
      summary: List in-flight session runs for a workflow
      parameters:
      - $ref: '#/components/parameters/workflowId'
      responses:
        '200':
          description: Run summaries
          content:
            application/json:
              schema:
                type: object
                properties:
                  runs:
                    type: array
                    items:
                      $ref: '#/components/schemas/RunSummary'
    post:
      tags:
      - Session Runs
      summary: Create an empty session run
      description: 'Returns a fresh `runId` you can subsequently feed via

        `POST /api/runs/{runId}/ingest`. Honors the workflow''s

        run-config TTL (default 1h before auto-expire).

        '
      parameters:
      - $ref: '#/components/parameters/workflowId'
      responses:
        '200':
          description: Run created
          content:
            application/json:
              schema:
                type: object
                properties:
                  runId:
                    type: string
  /api/workflow/{workflowId}/runs/clear-completed:
    delete:
      tags:
      - Session Runs
      summary: Bulk-delete completed/failed/cancelled runs for this workflow
      parameters:
      - $ref: '#/components/parameters/workflowId'
      responses:
        '200':
          description: Cleared
          content:
            application/json:
              schema:
                type: object
                properties:
                  removed:
                    type: integer
  /api/workflow/{workflowId}/ingest:
    post:
      tags:
      - Session Runs
      summary: 'Auto-Session: ingest data; create-or-find an active run'
      description: 'Convenience endpoint for "fire and forget" ingestion: looks up the

        currently-active run for this workflow (or creates one if none

        exists), then ingests the dataset payload. When trigger conditions

        are met, the run starts automatically (if `autoStart=true`).


        Use this from upstream systems that don''t track run IDs — they

        just push data, Orkidata coalesces.

        '
      parameters:
      - $ref: '#/components/parameters/workflowId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IngestRequest'
      responses:
        '200':
          description: Ingested (run may now be eligible to start)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowIngestResponse'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/workflow/{workflowId}/start:
    post:
      tags:
      - Session Runs
      summary: 'Auto-Session: trigger the active run if conditions met'
      description: 'Companion to `/ingest` — checks whether the active run''s trigger

        conditions are satisfied, and if so, starts execution. No-op if

        conditions aren''t met yet.

        '
      parameters:
      - $ref: '#/components/parameters/workflowId'
      responses:
        '200':
          description: Trigger evaluation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunExecutionResult'
  /api/workflow/{workflowId}/status:
    get:
      tags:
      - Session Runs
      summary: 'Auto-Session: status of the active run for a workflow'
      parameters:
      - $ref: '#/components/parameters/workflowId'
      responses:
        '200':
          description: Status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowStatus'
  /api/runs/{runId}:
    get:
      tags:
      - Session Runs
      summary: Get a session run's full state
      parameters:
      - $ref: '#/components/parameters/runId'
      - name: includeItems
        in: query
        required: false
        schema:
          type: boolean
          default: false
        description: Include the actual ingested items (omit by default — large)
      responses:
        '200':
          description: Run
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Run'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
      - Session Runs
      summary: Delete a session run
      parameters:
      - $ref: '#/components/parameters/runId'
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /api/runs/{runId}/ingest:
    post:
      tags:
      - Session Runs
      summary: Ingest data into a specific run
      parameters:
      - $ref: '#/components/parameters/runId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IngestRequest'
      responses:
        '200':
          description: Ingested
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestResponse'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/runs/{runId}/start:
    post:
      tags:
      - Session Runs
      summary: Trigger execution for a specific run
      parameters:
      - $ref: '#/components/parameters/runId'
      responses:
        '200':
          description: Execution result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunExecutionResult'
        '400':
          description: Trigger conditions not met
  /api/runs/{runId}/cancel:
    post:
      tags:
      - Session Runs
      summary: Cancel a session run (cooperative)
      parameters:
      - $ref: '#/components/parameters/runId'
      responses:
        '200':
          description: Cancel sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /api/workflow/{workflowId}/run-config:
    get:
      tags:
      - Session Runs
      summary: Get the workflow's run configuration
      parameters:
      - $ref: '#/components/parameters/workflowId'
      responses:
        '200':
          description: Run config
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunConfig'
    put:
      tags:
      - Session Runs
      summary: Save run configuration (trigger conditions, auto-start, schedule, etc.)
      parameters:
      - $ref: '#/components/parameters/workflowId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RunConfig'
      responses:
        '200':
          description: Saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /api/workflow/{workflowId}/usage:
    get:
      tags:
      - Workflows
      summary: List dashboards that reference this workflow
      description: 'Returns the dashboards (own + shared) that include this workflow

        in any widget. Scoped to the caller''s accessible dashboards

        only — does not leak non-shared dashboards from other owners.

        '
      parameters:
      - $ref: '#/components/parameters/workflowId'
      responses:
        '200':
          description: Dashboards using this workflow
          content:
            application/json:
              schema:
                type: object
                properties:
                  dashboards:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        shared:
                          type: boolean
                        owner_name:
                          type: string
  /api/workflow/{workflowId}/history:
    get:
      tags:
      - Execution History
      summary: List historical runs (paginated, slim)
      description: 'Returns slim run metadata (id, status, duration, timestamps) without

        the full output payload — the latter lives on each run document and

        is fetched via `/history/run/{runId}`.

        '
      parameters:
      - $ref: '#/components/parameters/workflowId'
      responses:
        '200':
          description: History entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  runs:
                    type: array
                    items:
                      $ref: '#/components/schemas/HistoryEntry'
    post:
      tags:
      - Execution History
      summary: Manually append a run to history (internal)
      description: 'Internal endpoint used by the worker to write completed runs.

        Not intended for direct caller use.

        '
      parameters:
      - $ref: '#/components/parameters/workflowId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HistoryEntry'
      responses:
        '200':
          description: Appended
  /api/workflow/{workflowId}/history/{runId}:
    delete:
      tags:
      - Execution History
      summary: Delete a specific run from history
      description: Cleans up associated step metrics and any export files this run produced.
      parameters:
      - $ref: '#/components/parameters/workflowId'
      - $ref: '#/components/parameters/runId'
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/workflow/{workflowId}/history/run/{runId}:
    get:
      tags:
      - Execution History
      summary: Get the full result of a specific run
      description: 'Returns the complete `result.steps[]` data including each step''s

        output payload — used by the UI to render the execution detail

        view and by automation that wants to inspect step-by-step output.

        '
      parameters:
      - $ref: '#/components/parameters/workflowId'
      - $ref: '#/components/parameters/runId'
      responses:
        '200':
          description: Full run
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/HistoryEntry'
                - type: object
                  properties:
                    result:
                      $ref: '#/components/schemas/ExecutionResult'
  /api/workflow/{workflowId}/history/clear:
    delete:
      tags:
      - Execution History
      summary: Wipe ALL execution history for this workflow
      description: 'Cascades into per-step and per-day metrics for this workflow.

        Idempotent. Export files generated by past runs are not actively

        deleted on this call — they expire automatically a short time

        after the run.

        '
      parameters:
      - $ref: '#/components/parameters/workflowId'
      responses:
        '200':
          description: Cleared
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /api/workflow/{workflowId}/history/{runId}/export/{stepId}:
    post:
      tags:
      - Execution History
      summary: Get a presigned download URL for an export step's output
      description: 'Returns the export step''s output file. For async runs the

        response is JSON `{ downloadUrl, filename }` — follow the

        presigned URL within 5 minutes. For sync runs the response body

        is the file bytes directly.

        '
      parameters:
      - $ref: '#/components/parameters/workflowId'
      - $ref: '#/components/parameters/runId'
      - name: stepId
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: 'Either a JSON `{ downloadUrl, filename }` (async runs) OR a

            binary file body (sync runs). Caller should follow `downloadUrl`

            if present, otherwise treat the response body as the file.

            '
          content:
            application/json:
              schema:
                type: object
                properties:
                  downloadUrl:
                    type: string
                    format: uri
                  filename:
                    type: string
                  downloads:
                    type: object
                    description: For return_data steps with multiple outputs — map of output-name to {downloadUrl,
                      filename}
                    additionalProperties:
                      type: object
                      properties:
                        downloadUrl:
                          type: string
                          format: uri
                        filename:
                          type: string
            application/octet-stream:
              schema:
                type: string
                format: binary
  /api/dashboards:
    get:
      tags:
      - Dashboards
      summary: Get the user's dashboard tree
      responses:
        '200':
          description: Dashboard tree
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DashboardList'
    post:
      tags:
      - Dashboards
      summary: Replace the dashboard tree
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DashboardList'
      responses:
        '200':
          description: Saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /api/dashboards/{dashboardId}:
    get:
      tags:
      - Dashboards
      summary: Get a single dashboard with widgets
      parameters:
      - $ref: '#/components/parameters/dashboardId'
      responses:
        '200':
          description: Dashboard
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Dashboard'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/dashboards/{dashboardId}/widgets:
    patch:
      tags:
      - Dashboards
      summary: Update only the widgets of a dashboard
      description: Saves widget positions/sizes/configs without touching dashboard metadata. Editor permission
        required.
      parameters:
      - $ref: '#/components/parameters/dashboardId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: '#/components/schemas/Widget'
      responses:
        '200':
          description: Saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/dashboards/cleanup:
    post:
      tags:
      - Dashboards
      summary: Bulk-delete dashboards
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - dashboardIds
              properties:
                dashboardIds:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: Done
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /api/dashboard/stats:
    get:
      tags:
      - Dashboards
      summary: Aggregated stats for the index/dashboard view
      description: 'Returns workflow counts, recent activity, active runs, scheduled

        runs, success rate. Scoped to caller''s own + shared workflows

        only (cross-tenant-safe).

        '
      parameters:
      - name: dashboardId
        in: query
        required: false
        schema:
          type: string
        description: Restrict to a specific dashboard's workflowIds
      - name: workflowIds
        in: query
        required: false
        schema:
          type: string
        description: Comma-separated workflow ids (alternative to dashboardId)
      - name: rowCount
        in: query
        required: false
        schema:
          type: integer
          default: 10
          maximum: 200
      responses:
        '200':
          description: Stats
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DashboardStats'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/widget-data/summary:
    get:
      tags:
      - Widget Data
      summary: Aggregated workflow-summary widget data
      description: Total executions, last run, freshness window across the requested workflowIds.
      parameters:
      - name: workflowIds
        in: query
        required: false
        schema:
          type: string
          description: Comma-separated workflow ids; defaults to all owned workflows
      responses:
        '200':
          description: Summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetSummaryResponse'
    post:
      tags:
      - Widget Data
      summary: Same as GET, but accepts large workflowId lists in body
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                workflowIds:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: Summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetSummaryResponse'
  /api/widget-data/trend:
    get:
      tags:
      - Widget Data
      summary: Time-series execution-volume + duration trend
      parameters:
      - name: workflowIds
        in: query
        required: false
        schema:
          type: string
      - name: window
        in: query
        required: false
        schema:
          type: string
          enum:
          - 24h
          - 7d
          - 30d
          - 90d
          default: 7d
      responses:
        '200':
          description: Trend
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetTrendResponse'
    post:
      tags:
      - Widget Data
      summary: Same as GET, body-based
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                workflowIds:
                  type: array
                  items:
                    type: string
                window:
                  type: string
      responses:
        '200':
          description: Trend
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetTrendResponse'
  /api/widget-data/step-metrics:
    post:
      tags:
      - Widget Data
      summary: Per-step execution metrics for a workflow
      description: 'Returns avg/p95 duration, row counts, column history per step.

        Used by the dashboard''s step-metrics widget.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - workflowId
              properties:
                workflowId:
                  type: string
      responses:
        '200':
          description: Step metrics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowStepMetrics'
  /api/data-storage:
    get:
      tags:
      - Data Storage
      summary: Get the user's file/folder tree
      responses:
        '200':
          description: Tree
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataStorageTree'
    post:
      tags:
      - Data Storage
      summary: Replace the data-storage tree
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DataStorageTree'
      responses:
        '200':
          description: Saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /api/data-storage/{fileId}:
    delete:
      tags:
      - Data Storage
      summary: Delete a file or folder (and its S3 object if applicable)
      parameters:
      - $ref: '#/components/parameters/fileId'
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /api/data-storage/upload-url:
    get:
      tags:
      - Data Storage
      summary: Get a presigned S3 PUT URL for direct browser-to-S3 upload
      description: 'Two-step upload: client first hits this for a presigned PUT URL +

        target S3 key, then PUTs the file bytes directly to S3, then calls

        `/confirm-upload` to register metadata. Bypasses API Gateway''s

        10MB body limit.

        '
      parameters:
      - name: filename
        in: query
        required: true
        schema:
          type: string
      - name: contentType
        in: query
        required: false
        schema:
          type: string
      responses:
        '200':
          description: Presigned URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  uploadUrl:
                    type: string
                    format: uri
                  s3Key:
                    type: string
                  fields:
                    type: object
                    additionalProperties: true
                    description: Required POST fields if using POST-form upload
  /api/data-storage/confirm-upload:
    post:
      tags:
      - Data Storage
      summary: Register a finished single-file upload
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConfirmUploadRequest'
      responses:
        '200':
          description: Registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataStorageItem'
  /api/data-storage/confirm-upload-batch:
    post:
      tags:
      - Data Storage
      summary: Register multiple finished uploads at once (bulk import)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - files
              properties:
                files:
                  type: array
                  items:
                    $ref: '#/components/schemas/ConfirmUploadRequest'
      responses:
        '200':
          description: Registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  files:
                    type: array
                    items:
                      $ref: '#/components/schemas/DataStorageItem'
  /api/data-storage/preview/{fileId}:
    get:
      tags:
      - Data Storage
      summary: Preview the first N rows of a file (for the UI table view)
      parameters:
      - $ref: '#/components/parameters/fileId'
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          default: 100
          maximum: 1000
      - name: sheet
        in: query
        required: false
        schema:
          type: string
          description: Sheet name (Excel files only)
      responses:
        '200':
          description: Preview
          content:
            application/json:
              schema:
                type: object
                properties:
                  rows:
                    type: array
                    items:
                      type: object
                  columns:
                    type: array
                    items:
                      type: string
                  rowCount:
                    type: integer
                  truncated:
                    type: boolean
  /api/data-storage/columns/{fileId}:
    get:
      tags:
      - Data Storage
      summary: Get just the column names of a file (cheap header read)
      parameters:
      - $ref: '#/components/parameters/fileId'
      responses:
        '200':
          description: Columns
          content:
            application/json:
              schema:
                type: object
                properties:
                  columns:
                    type: array
                    items:
                      type: string
  /api/data-storage/content/{fileId}:
    get:
      tags:
      - Data Storage
      summary: Stream the entire file contents
      description: 'Used by the UI''s "view raw" feature for small files. Larger files

        should use `/download-url` for a direct S3 fetch.

        '
      parameters:
      - $ref: '#/components/parameters/fileId'
      responses:
        '200':
          description: Raw file bytes
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
  /api/data-storage/download-url/{fileId}:
    get:
      tags:
      - Data Storage
      summary: Get a presigned S3 GET URL for direct download
      parameters:
      - $ref: '#/components/parameters/fileId'
      responses:
        '200':
          description: Presigned URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  downloadUrl:
                    type: string
                    format: uri
                  filename:
                    type: string
  /api/data-storage/cleanup:
    post:
      tags:
      - Data Storage
      summary: Bulk-delete file entries
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - fileIds
              properties:
                fileIds:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: Done
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /api/connections:
    get:
      tags:
      - Connections
      summary: List all connections owned by the user
      responses:
        '200':
          description: Connections
          content:
            application/json:
              schema:
                type: object
                properties:
                  connections:
                    type: array
                    items:
                      $ref: '#/components/schemas/Connection'
    post:
      tags:
      - Connections
      summary: Create a new connection
      description: 'Connection credentials are encrypted at rest. The same request

        shape covers all supported drivers (`postgres`, `mysql`, `mssql`,

        `oracle`, `sftp`, `s3`); driver-specific fields are documented in

        the schema below.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConnectionCreate'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Connection'
        '400':
          $ref: '#/components/responses/BadRequest'
  /api/connections/test-new:
    post:
      tags:
      - Connections
      summary: Test a connection's credentials before saving
      description: 'Tries to open a connection with the supplied config and immediately

        closes it. Returns success/failure + connector-specific diagnostic

        info. No persistence.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConnectionCreate'
      responses:
        '200':
          description: Test result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectionTestResult'
  /api/connections/{connectionId}:
    get:
      tags:
      - Connections
      summary: Get connection metadata (credentials redacted)
      parameters:
      - $ref: '#/components/parameters/connectionId'
      responses:
        '200':
          description: Connection
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Connection'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - Connections
      summary: Update connection (re-encrypts credentials if changed)
      parameters:
      - $ref: '#/components/parameters/connectionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConnectionCreate'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Connection'
    delete:
      tags:
      - Connections
      summary: Delete a connection
      parameters:
      - $ref: '#/components/parameters/connectionId'
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /api/connections/{connectionId}/test:
    post:
      tags:
      - Connections
      summary: Test an existing connection
      parameters:
      - $ref: '#/components/parameters/connectionId'
      responses:
        '200':
          description: Test result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectionTestResult'
  /api/connections/{connectionId}/discover:
    get:
      tags:
      - Connections
      summary: Discover schema (tables, columns) on the connected data source
      parameters:
      - $ref: '#/components/parameters/connectionId'
      responses:
        '200':
          description: Schema
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectionDiscover'
  /api/connections/{connectionId}/preview:
    post:
      tags:
      - Connections
      summary: Run a query and preview the first N rows (read-only)
      parameters:
      - $ref: '#/components/parameters/connectionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - query
              properties:
                query:
                  type: string
                  description: SQL SELECT (read-only)
                limit:
                  type: integer
                  default: 100
                  maximum: 1000
                params:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          description: Preview
          content:
            application/json:
              schema:
                type: object
                properties:
                  rows:
                    type: array
                    items:
                      type: object
                  columns:
                    type: array
                    items:
                      type: string
  /api/connections/{connectionId}/write-target-info:
    post:
      tags:
      - Connections
      summary: Probe a destination table/path for write_to_db / write_to_sftp configuration
      parameters:
      - $ref: '#/components/parameters/connectionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                table:
                  type: string
                  description: SQL target (postgres/mysql/etc.)
                path:
                  type: string
                  description: SFTP path (sftp connections)
      responses:
        '200':
          description: Target metadata
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
  /api/email/recipients:
    get:
      tags:
      - Email Recipients
      summary: List the user's verified-recipients (for the send_email step picker)
      description: 'Returns every recipient on the user''s list, including pending,

        unsubscribed, bounced, and complained — the UI shows them all

        but only verified addresses can be selected for sending.

        Each entry includes its custom `tags` (default `[]`).

        '
      responses:
        '200':
          description: Recipients
          content:
            application/json:
              schema:
                type: object
                properties:
                  recipients:
                    type: array
                    items:
                      $ref: '#/components/schemas/Recipient'
    post:
      tags:
      - Email Recipients
      summary: Add a recipient and send a verification email
      description: 'Sends a tokenized verification email (1h TTL) to the address.

        The recipient clicks the link to flip status from pending to

        verified — no Orkidata account required.


        **Re-add policy** when the email already exists for this user:

        - `pending` → resend verification (cooldown enforced; 429 if too soon)

        - `verified` → 200 noop ("already verified")

        - `unsubscribed`/`complained` → 409 refuse (recipient must opt back in)

        - `bounced` → 409 refuse (must remove + re-add a corrected address)

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecipientAddRequest'
      responses:
        '201':
          description: Added (status=pending), verification email sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecipientAddResponse'
        '200':
          description: Already-verified noop OR re-sent verification (status field disambiguates)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecipientAddResponse'
        '202':
          description: Recipient added but email could not be sent right now (use Resend)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecipientAddResponse'
        '409':
          description: Address blocked from re-add (unsubscribed / bounced / complained)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
  /api/email/recipients/{recipientId}:
    delete:
      tags:
      - Email Recipients
      summary: Remove a recipient
      parameters:
      - $ref: '#/components/parameters/recipientId'
      responses:
        '200':
          description: Removed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/email/recipients/{recipientId}/tags:
    patch:
      tags:
      - Email Recipients
      summary: Replace the tag list on a recipient
      description: 'Tags are user-defined free-form labels (e.g. `ops`, `internal`,

        `client-acme`) used for grouping in the send_email step picker.

        Editing tags doesn''t trigger a verification email or change

        status. `tags: null` or missing field clears all tags.

        '
      parameters:
      - $ref: '#/components/parameters/recipientId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tags:
                  type: array
                  items:
                    type: string
                  description: List of strings; normalized at write (trim, lowercase, dedupe, ≤20 entries
                    × ≤32 chars)
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  recipient:
                    $ref: '#/components/schemas/Recipient'
                  message:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/billing/plans:
    get:
      tags:
      - Billing
      summary: Get the plan catalog
      description: 'Returns Free / Entry / Pro / Enterprise tiers with pricing,

        per-tier limits, and capability flags (`audit_logs`, `sso`,

        `seat_management` — all `false` today, forward-compat hooks).

        '
      responses:
        '200':
          description: Plans + add-ons
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanCatalog'
  /api/billing/subscription:
    get:
      tags:
      - Billing
      summary: Get the caller's current Stripe subscription state
      responses:
        '200':
          description: Subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Subscription'
  /api/usage:
    get:
      tags:
      - Usage
      summary: Get current usage vs quotas (RPM, exec, data, emails)
      responses:
        '200':
          description: Quota snapshot
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageQuotas'
  /api/usage/history:
    get:
      tags:
      - Usage
      summary: Per-day usage history (executions + data volume + duration)
      parameters:
      - name: start
        in: query
        required: false
        schema:
          type: string
          format: date
      - name: end
        in: query
        required: false
        schema:
          type: string
          format: date
      responses:
        '200':
          description: History
          content:
            application/json:
              schema:
                type: object
                properties:
                  days:
                    type: array
                    items:
                      $ref: '#/components/schemas/UsageDay'
  /api/usage/history/{dateStr}:
    get:
      tags:
      - Usage
      summary: Per-hour usage breakdown for a specific day
      parameters:
      - name: dateStr
        in: path
        required: true
        schema:
          type: string
          format: date
        description: YYYY-MM-DD in the user's local timezone
      responses:
        '200':
          description: Hourly breakdown
          content:
            application/json:
              schema:
                type: object
                properties:
                  date:
                    type: string
                    format: date
                  hours:
                    type: array
                    items:
                      type: object
                      properties:
                        hour:
                          type: integer
                          minimum: 0
                          maximum: 23
                        executions:
                          type: integer
                        durationSeconds:
                          type: number
                        dataMB:
                          type: number
components:
  parameters:
    workflowId:
      name: workflowId
      in: path
      required: true
      schema:
        type: string
      description: Workflow ID (`id_<timestamp>_<rand>`)
    runId:
      name: runId
      in: path
      required: true
      schema:
        type: string
      description: Run ID (`run_<timestamp>_<rand>` for sync; `async_id_<timestamp>_<hash>` for async)
    dashboardId:
      name: dashboardId
      in: path
      required: true
      schema:
        type: string
      description: Dashboard ID
    fileId:
      name: fileId
      in: path
      required: true
      schema:
        type: string
      description: Data Storage file ID
    connectionId:
      name: connectionId
      in: path
      required: true
      schema:
        type: string
      description: Connection ID
    recipientId:
      name: recipientId
      in: path
      required: true
      schema:
        type: string
      description: Recipient entry ID (per-user-doc 8-byte hex)
    teamId:
      name: teamId
      in: path
      required: true
      schema:
        type: string
      description: Team ID
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: ETL token
      description: 'Programmatic auth. Mint with `POST /api/auth/token`, include in

        every request as `Authorization: Bearer <token>`. Long-lived;

        revoke with `DELETE /api/auth/token`.

        '
  responses:
    Unauthorized:
      description: Missing or invalid session/token
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: Authenticated, but not authorized for this resource
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Resource not found (or hidden from this caller)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    BadRequest:
      description: Validation error in request body or query
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: Conflict with existing state (e.g. unsubscribed recipient re-add)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimited:
      description: Rate limit exceeded
      content:
        application/json:
          schema:
            allOf:
            - $ref: '#/components/schemas/ErrorResponse'
            - type: object
              properties:
                retry_after:
                  type: integer
                  description: Seconds until next attempt is allowed
  schemas:
    SuccessResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
    MessageResponse:
      type: object
      properties:
        message:
          type: string
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          example: Resource not found
      required:
      - error
    User:
      type: object
      properties:
        user_id:
          type: string
        email:
          type: string
          format: email
        name:
          type: string
        tier:
          type: string
          enum:
          - free
          - entry
          - pro
          - enterprise
        email_confirmed:
          type: boolean
        timezone:
          type: string
          description: IANA timezone (e.g. `America/Mexico_City`)
        limits:
          $ref: '#/components/schemas/UsageQuotas'
    UserProfile:
      allOf:
      - $ref: '#/components/schemas/User'
      - type: object
        properties:
          has_api_token:
            type: boolean
          created_at:
            type: string
            format: date-time
          last_login:
            type: string
            format: date-time
    RegisterRequest:
      type: object
      required:
      - email
      - password
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          description: ≥10 chars
          ≥1 uppercase: null
          ≥1 digit: null
        name:
          type: string
        timezone:
          type: string
          description: IANA timezone (defaults to UTC)
    LoginRequest:
      type: object
      required:
      - email
      - password
      properties:
        email:
          type: string
          format: email
        password:
          type: string
        timezone:
          type: string
          description: IANA timezone (captured at login)
        next:
          type: string
          description: Optional same-origin path to redirect to after login
    LoginResponse:
      type: object
      properties:
        user_id:
          type: string
        email:
          type: string
        name:
          type: string
        tier:
          type: string
        redirect:
          type: string
          description: Server-validated redirect target — clients must trust this
          never the raw `next` query param: null
    PasswordChangeRequest:
      type: object
      required:
      - current_password
      - new_password
      properties:
        current_password:
          type: string
        new_password:
          type: string
    ApiTokenResponse:
      type: object
      properties:
        token:
          type: string
          description: Plaintext API token (shown ONCE — never recoverable)
        created_at:
          type: string
          format: date-time
    WorkflowList:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowItem'
        confirmEmpty:
          type: boolean
          description: Required on POST when `items` is `[]` and the stored tree is non-empty
    WorkflowItem:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum:
          - workflow
          - folder
        name:
          type: string
        timezone:
          type: string
        createdAt:
          type: string
          format: date-time
        lastRunAt:
          type: string
          format: date-time
          nullable: true
        lastRunParams:
          type: object
          additionalProperties: true
        children:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowItem'
    WorkflowDefinition:
      type: object
      properties:
        steps:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowStep'
    WorkflowStep:
      type: object
      description: 'Step types: load_data, print_data, join_data, select_columns, filter_rows,

        split_column, return_data, export_file, move_file, webhook, stop,

        conditional, send_email, write_to_db, write_to_sftp.

        Per-step config schemas are documented in `docs/API.md` § Step Types Reference.

        '
      properties:
        id:
          type: string
        type:
          type: string
        name:
          type: string
        config:
          type: object
          additionalProperties: true
    ExecutionResult:
      type: object
      properties:
        workflow_id:
          type: string
        success:
          type: boolean
        started_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
        duration_seconds:
          type: number
        step_count:
          type: integer
        steps:
          type: array
          items:
            $ref: '#/components/schemas/StepResult'
        error:
          type: string
          nullable: true
        timed_out:
          type: boolean
        timed_out_step:
          type: string
          nullable: true
    StepResult:
      type: object
      properties:
        step_id:
          type: string
        step_name:
          type: string
        step_type:
          type: string
        success:
          type: boolean
        executed_at:
          type: string
          format: date-time
        output:
          type: object
          additionalProperties: true
        message:
          type: string
        error:
          type: string
          nullable: true
    AsyncDispatchResponse:
      type: object
      properties:
        async:
          type: boolean
          example: true
        runId:
          type: string
          example: async_id_17772_a0c5ffd8
        tier:
          type: integer
          enum:
          - 2
          - 3
    AsyncRunStatus:
      type: object
      properties:
        run_id:
          type: string
        status:
          type: string
          enum:
          - queued
          - running
          - completed
          - failed
          - timeout
          - cancelled
        current_step:
          type: string
        current_step_index:
          type: integer
        total_steps:
          type: integer
        progress_pct:
          type: integer
        started_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
        error:
          type: string
          nullable: true
        result_summary:
          type: object
          properties:
            duration_seconds:
              type: number
            success:
              type: boolean
        last_heartbeat:
          type: number
          description: Unix epoch seconds — timestamp of this run's last reported progress
    IngestRequest:
      type: object
      required:
      - dataset
      - items
      properties:
        dataset:
          type: string
          description: Name of the dataset (matches workflow's `expectedDatasets`)
        items:
          type: array
          items:
            type: object
            additionalProperties: true
        mode:
          type: string
          enum:
          - append
          - replace
          default: append
    IngestResponse:
      type: object
      properties:
        success:
          type: boolean
        dataset:
          type: string
        itemCount:
          type: integer
        runStatus:
          type: string
        conditionsMet:
          type: boolean
    WorkflowIngestResponse:
      allOf:
      - $ref: '#/components/schemas/IngestResponse'
      - type: object
        properties:
          runId:
            type: string
          runCreated:
            type: boolean
    RunExecutionResult:
      type: object
      properties:
        success:
          type: boolean
        runId:
          type: string
        status:
          type: string
        duration_seconds:
          type: number
        step_count:
          type: integer
        data:
          type: object
          description: Transformed data (only when `returnDataInResponse=true` in run config)
          additionalProperties: true
    Run:
      type: object
      properties:
        runId:
          type: string
        workflowId:
          type: string
        status:
          type: string
          enum:
          - pending
          - collecting
          - ready
          - running
          - completed
          - failed
          - cancelled
          - expired
        createdAt:
          type: string
          format: date-time
        ttlSeconds:
          type: integer
        datasets:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/Dataset'
        logs:
          type: array
          items:
            $ref: '#/components/schemas/LogEntry'
        conditionsMet:
          type: boolean
        unmetConditions:
          type: array
          items:
            type: string
        lastIngestAt:
          type: string
          format: date-time
    RunSummary:
      type: object
      properties:
        runId:
          type: string
        workflowId:
          type: string
        status:
          type: string
        createdAt:
          type: string
          format: date-time
        datasets:
          type: object
          additionalProperties:
            type: object
            properties:
              count:
                type: integer
              lastUpdated:
                type: string
                format: date-time
        conditionsMet:
          type: boolean
        ttlSeconds:
          type: integer
    Dataset:
      type: object
      properties:
        items:
          type: array
          items:
            type: object
            additionalProperties: true
        count:
          type: integer
        lastUpdated:
          type: string
          format: date-time
    LogEntry:
      type: object
      properties:
        timestamp:
          type: string
          format: date-time
        level:
          type: string
          enum:
          - info
          - warning
          - error
        message:
          type: string
    RunConfig:
      type: object
      properties:
        expectedDatasets:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              description:
                type: string
        triggerConfig:
          type: object
          properties:
            requiredDatasets:
              type: array
              items:
                type: string
            minCounts:
              type: object
              additionalProperties:
                type: integer
            autoStart:
              type: boolean
              default: false
            debounceSeconds:
              type: integer
              default: 0
            cronCondition:
              type: object
              properties:
                enabled:
                  type: boolean
                expression:
                  type: string
                  description: 5-field cron in workflow's local timezone
        scheduledCron:
          type: object
          description: Workflow-level scheduled run — fires automatically on the configured cron expression
          properties:
            enabled:
              type: boolean
            expression:
              type: string
            inputParams:
              type: object
              additionalProperties: true
            paramsSource:
              type: string
              enum:
              - lastRun
              - custom
              - dataStorage
            dsParamsFile:
              type: object
              properties:
                fileId:
                  type: string
                path:
                  type: string
            lastRun:
              type: string
              format: date-time
        ttlSeconds:
          type: integer
          default: 3600
        returnDataInResponse:
          type: boolean
          default: false
        autoSessionMode:
          type: boolean
          default: false
        timeouts:
          type: object
          properties:
            workflow_timeout:
              type: integer
              description: Seconds, clamped to [5, WORKFLOW_TIMEOUT_MAX]
            step_timeout:
              type: integer
              nullable: true
    WorkflowStatus:
      type: object
      properties:
        hasActiveRun:
          type: boolean
        runId:
          type: string
        status:
          type: string
        conditionsMet:
          type: boolean
        unmetConditions:
          type: array
          items:
            type: string
        datasets:
          type: object
          additionalProperties:
            type: object
            properties:
              count:
                type: integer
        config:
          type: object
          properties:
            autoSessionMode:
              type: boolean
            autoStart:
              type: boolean
            returnDataInResponse:
              type: boolean
    HistoryEntry:
      type: object
      properties:
        id:
          type: string
        workflowId:
          type: string
        startedAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
        duration:
          type: number
        status:
          type: string
          enum:
          - success
          - failed
          - running
          - cancelled
          - timeout
        inputParams:
          type: object
          additionalProperties: true
        apiTriggered:
          type: boolean
        sessionRunId:
          type: string
        tier:
          type: integer
          enum:
          - 1
          - 2
          - 3
        stepCount:
          type: integer
        hasExports:
          type: boolean
        exportSummary:
          type: array
          items:
            type: object
            properties:
              stepId:
                type: string
              name:
                type: string
              rows:
                type: integer
              delimiter:
                type: string
        dataMetrics:
          type: object
          properties:
            totalInputRows:
              type: integer
            totalOutputRows:
              type: integer
    DashboardList:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/DashboardItem'
    DashboardItem:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum:
          - dashboard
          - folder
        name:
          type: string
        children:
          type: array
          items:
            $ref: '#/components/schemas/DashboardItem'
        widgets:
          type: array
          items:
            $ref: '#/components/schemas/Widget'
        workflowIds:
          type: array
          items:
            type: string
          description: Workflow IDs the dashboard's widgets reference
    Dashboard:
      allOf:
      - $ref: '#/components/schemas/DashboardItem'
      - type: object
        properties:
          permission:
            type: string
            enum:
            - owner
            - editor
            - viewer
    Widget:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum:
          - summary
          - trend
          - step-metrics
          - custom
        x:
          type: integer
        y:
          type: integer
        w:
          type: integer
        h:
          type: integer
        config:
          type: object
          additionalProperties: true
    DashboardStats:
      type: object
      properties:
        totalWorkflows:
          type: integer
        activeRunsCount:
          type: integer
        recentExecutionsCount:
          type: integer
          description: Last 24h
        successRate:
          type: integer
          description: Percent
        recentActivity:
          type: array
          items:
            $ref: '#/components/schemas/RecentActivityEntry'
        activeRuns:
          type: array
          items:
            type: object
            properties:
              runId:
                type: string
              workflowId:
                type: string
              workflowName:
                type: string
              status:
                type: string
              datasetCount:
                type: integer
              lastActivity:
                type: string
                format: date-time
        scheduledRuns:
          type: array
          items:
            type: object
            properties:
              workflowId:
                type: string
              workflowName:
                type: string
              timezone:
                type: string
              schedules:
                type: array
                items:
                  type: object
                  properties:
                    type:
                      type: string
                      enum:
                      - cron
                      - session
                    expression:
                      type: string
                    lastRun:
                      type: string
                      format: date-time
        scheduledRunsCount:
          type: integer
    RecentActivityEntry:
      type: object
      properties:
        id:
          type: string
        runId:
          type: string
        workflowId:
          type: string
        workflowName:
          type: string
        status:
          type: string
        duration:
          type: number
        startedAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
        completedAtFormatted:
          type: string
        timezoneAbbrev:
          type: string
        tier:
          type: integer
        dataMetrics:
          type: object
          properties:
            inputRows:
              type: integer
            outputRows:
              type: integer
    WidgetSummaryResponse:
      type: object
      properties:
        totalExecutions:
          type: integer
        lastRun:
          type: string
          format: date-time
        workflowFreshness:
          type: object
          additionalProperties:
            type: number
            description: Epoch seconds of last successful run per workflow
    WidgetTrendResponse:
      type: object
      properties:
        window:
          type: string
        series:
          type: array
          items:
            type: object
            properties:
              t:
                type: string
                format: date-time
              executions:
                type: integer
              avgDurationMs:
                type: number
              successRate:
                type: number
    WorkflowStepMetrics:
      type: object
      properties:
        steps:
          type: array
          items:
            type: object
            properties:
              stepId:
                type: string
              stepName:
                type: string
              stepType:
                type: string
              executions:
                type: integer
              lastRun:
                type: string
                format: date-time
              avgDurationMs:
                type: number
              p95DurationMs:
                type: number
              avgInputRows:
                type: number
              avgOutputRows:
                type: number
              lastColumns:
                type: array
                items:
                  type: string
              lastRenames:
                type: object
                additionalProperties:
                  type: string
              lastDropped:
                type: array
                items:
                  type: string
              columnHistory:
                type: array
                items:
                  type: object
                  properties:
                    columns:
                      type: array
                      items:
                        type: string
                    firstSeen:
                      type: string
                      format: date-time
                    lastSeen:
                      type: string
                      format: date-time
        dailyVolume:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
                format: date
              totalInputRows:
                type: integer
              totalOutputRows:
                type: integer
              executions:
                type: integer
              avgDurationMs:
                type: number
        lastUpdatedAt:
          type: string
          format: date-time
    DataStorageTree:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/DataStorageItem'
    DataStorageItem:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum:
          - file
          - folder
        name:
          type: string
        fileFormat:
          type: string
          enum:
          - csv
          - tsv
          - psv
          - ssv
          - json
          - xml
          - xlsx
          - xls
          - parquet
        rowCount:
          type: integer
        columnCount:
          type: integer
        sizeBytes:
          type: integer
        s3Key:
          type: string
        uploadedAt:
          type: string
          format: date-time
        children:
          type: array
          items:
            $ref: '#/components/schemas/DataStorageItem'
    ConfirmUploadRequest:
      type: object
      required:
      - s3Key
      - filename
      properties:
        s3Key:
          type: string
          description: S3 key returned by /upload-url
        filename:
          type: string
        targetFolderId:
          type: string
          description: Folder to drop the file into (defaults to root)
        ownerId:
          type: string
          description: For shared-folder uploads — the folder owner's user_id
    Connection:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        driver:
          type: string
          enum:
          - postgres
          - mysql
          - mssql
          - oracle
          - sftp
          - s3
        host:
          type: string
        port:
          type: integer
        username:
          type: string
        database:
          type: string
          nullable: true
        pathPrefix:
          type: string
          description: SFTP only
        hostKeyFingerprint:
          type: string
          description: SFTP only — pinned host-key fingerprint
        createdAt:
          type: string
          format: date-time
    ConnectionCreate:
      allOf:
      - $ref: '#/components/schemas/Connection'
      - type: object
        properties:
          password:
            type: string
            description: One of password / privateKey / awsAccessKey depending on driver
          privateKey:
            type: string
            description: PEM (SFTP)
          privateKeyPassphrase:
            type: string
          secretsManagerArn:
            type: string
            description: Alternative to inline credentials — points at a Secrets Manager secret
    ConnectionTestResult:
      type: object
      properties:
        success:
          type: boolean
        latencyMs:
          type: number
        message:
          type: string
        diagnostic:
          type: object
          additionalProperties: true
    ConnectionDiscover:
      type: object
      properties:
        tables:
          type: array
          items:
            type: object
            properties:
              schema:
                type: string
              name:
                type: string
              columns:
                type: array
                items:
                  type: object
                  properties:
                    name:
                      type: string
                    type:
                      type: string
                    nullable:
                      type: boolean
    Recipient:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        status:
          type: string
          enum:
          - pending
          - verified
          - unsubscribed
          - bounced
          - complained
        added_at:
          type: string
          format: date-time
        verified_at:
          type: string
          format: date-time
          nullable: true
        last_used_at:
          type: string
          format: date-time
          nullable: true
        tags:
          type: array
          items:
            type: string
          description: User-defined free-form labels (max 20 entries × 32 chars, normalized lowercase)
    RecipientAddRequest:
      type: object
      required:
      - email
      properties:
        email:
          type: string
          format: email
        tags:
          type: array
          items:
            type: string
          description: Optional tags applied at creation time
    RecipientAddResponse:
      type: object
      properties:
        message:
          type: string
        recipient:
          $ref: '#/components/schemas/Recipient'
        status:
          type: string
          enum:
          - added
          - resent
          - already_verified
          - added_no_email
          description: Disambiguates the 200 / 201 / 202 response codes
        retry_after:
          type: integer
          description: Present on 202 when verification email failed
    Team:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        owner_id:
          type: string
        members:
          type: array
          items:
            $ref: '#/components/schemas/TeamMember'
        member_count:
          type: integer
        created_at:
          type: string
          format: date-time
    TeamMember:
      type: object
      properties:
        user_id:
          type: string
        role:
          type: string
          enum:
          - owner
          - admin
          - member
        name:
          type: string
        email:
          type: string
        joined_at:
          type: string
          format: date-time
    TeamInvitation:
      type: object
      properties:
        team_id:
          type: string
        team_name:
          type: string
        invited_by:
          type: string
        invited_at:
          type: string
          format: date-time
    Share:
      type: object
      properties:
        id:
          type: string
        owner_id:
          type: string
        owner_name:
          type: string
        owner_email:
          type: string
        item_id:
          type: string
        item_type:
          type: string
          enum:
          - workflow
          - folder
          - dashboard
          - file
        resource_type:
          type: string
          enum:
          - workflows
          - dashboards
          - data_storage
        permission:
          type: string
          enum:
          - viewer
          - editor
        shared_with_user_id:
          type: string
          nullable: true
        shared_with_team_id:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    ShareCreateRequest:
      type: object
      required:
      - item_id
      - item_type
      - resource_type
      - permission
      properties:
        item_id:
          type: string
        item_type:
          type: string
          enum:
          - workflow
          - folder
          - dashboard
          - file
        resource_type:
          type: string
          enum:
          - workflows
          - dashboards
          - data_storage
        permission:
          type: string
          enum:
          - viewer
          - editor
        shared_with_user_id:
          type: string
          description: Mutually exclusive with shared_with_team_id
        shared_with_team_id:
          type: string
    SharedItem:
      type: object
      properties:
        share_id:
          type: string
        item:
          type: object
          additionalProperties: true
          description: The actual workflow/dashboard/file item
        item_type:
          type: string
        permission:
          type: string
          enum:
          - viewer
          - editor
        owner_name:
          type: string
        owner_email:
          type: string
        owner_id:
          type: string
        is_team_share:
          type: boolean
    PlanCatalog:
      type: object
      properties:
        plans:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                enum:
                - free
                - entry
                - pro
                - enterprise
              name:
                type: string
              price_monthly_usd:
                type: number
                nullable: true
                description: null = "Custom" (Enterprise)
              limits:
                type: object
                description: Limits keyed by metric name (rpm, exec_per_hour, max_data_mb, max_emails_per_day,
                  etc.)
                additionalProperties: true
              capabilities:
                type: object
                properties:
                  audit_logs:
                    type: boolean
                  sso:
                    type: boolean
                  seat_management:
                    type: boolean
        addons:
          type: array
          items:
            type: object
            additionalProperties: true
    Subscription:
      type: object
      properties:
        tier:
          type: string
          enum:
          - free
          - entry
          - pro
          - enterprise
        status:
          type: string
          description: Stripe subscription status
        stripe_customer_id:
          type: string
          nullable: true
        stripe_subscription_id:
          type: string
          nullable: true
        current_period_start:
          type: string
          format: date-time
          nullable: true
        current_period_end:
          type: string
          format: date-time
          nullable: true
        cancel_at_period_end:
          type: boolean
    UsageQuotas:
      type: object
      properties:
        rpm:
          type: integer
        read_rpm:
          type: integer
        exec_per_hour:
          type: integer
        concurrent:
          type: integer
        max_data_mb:
          type: integer
        max_exec_seconds:
          type: integer
        max_emails_per_day:
          type: integer
        used:
          type: object
          properties:
            rpm:
              type: integer
            read_rpm:
              type: integer
            exec_per_hour:
              type: integer
            concurrent:
              type: integer
            emails_today:
              type: integer
    UsageDay:
      type: object
      properties:
        date:
          type: string
          format: date
        executions:
          type: integer
        durationSeconds:
          type: number
        dataMB:
          type: number
        successRate:
          type: number
          description: Percent
    ResendWebhookEvent:
      type: object
      description: 'Resend webhook event shape. Verify the Svix signature before

        trusting any field. See `bounce_handler.py` for the canonical

        verification recipe.

        '
      properties:
        type:
          type: string
          enum:
          - email.bounced
          - email.complained
          - email.delivered
          - email.sent
          - email.opened
          - email.clicked
        created_at:
          type: string
          format: date-time
        data:
          type: object
          properties:
            email_id:
              type: string
            from:
              type: string
            to:
              type: array
              items:
                type: string
            subject:
              type: string
            bounce:
              type: object
              description: Present on `email.bounced` events
              properties:
                type:
                  type: string
                  enum:
                  - Permanent
                  - Transient
                  - Undetermined
                code:
                  type: string
                message:
                  type: string
            complaint:
              type: object
              description: Present on `email.complained` events
              properties:
                type:
                  type: string
                user_agent:
                  type: string
