{
  "openapi": "3.0.3",
  "info": {
    "title": "Loopster API",
    "description": "The Loopster API allows you to programmatically manage tables, columns, rows, and processing in your Loopster workspace.",
    "version": "1.0.0",
    "contact": {
      "email": "support@loopster.ai",
      "url": "https://docs.loopster.ai"
    }
  },
  "servers": [
    {
      "url": "https://app.loopster.ai/api",
      "description": "Production server"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Projects",
      "description": "Manage Loopster projects"
    },
    {
      "name": "Columns",
      "description": "Manage columns within a project"
    },
    {
      "name": "Rows",
      "description": "Manage rows within a project"
    },
    {
      "name": "Cells",
      "description": "Batch update cells within a project"
    },
    {
      "name": "Processing",
      "description": "Trigger and manage row processing"
    },
    { "name": "Imports", "description": "Import files into tables" },
    { "name": "Schedules", "description": "Run columns on a schedule" },
    { "name": "Exports", "description": "Export tables as files" },
    {
      "name": "Views",
      "description": "Manage views within a project"
    },
    {
      "name": "Share links",
      "description": "Public, read-only links to a table. Every operation needs an editor key, because each URL is a public read of the whole table."
    },
    {
      "name": "Feedback",
      "description": "Submit feedback"
    },
    {
      "name": "Capabilities",
      "description": "Run one enrichment action on inputs, without building a table."
    },
    {
      "name": "Functions",
      "description": "Run reusable workflows (Functions) that a workspace has enabled for agents"
    },
    {
      "name": "Account",
      "description": "Read the wallet balance, plan and spend of the workspace the API key belongs to"
    },
    { "name": "Webhooks", "description": "Outbound webhooks: URLs Loopster tells when a run settles." }
  ],
  "paths": {
    "/v1/tables/{tableId}/imports/upload-url": {
      "post": {
        "operationId": "createImportUploadUrl",
        "summary": "Get an upload address",
        "description": "An address to upload one CSV or JSON-lines file to, for importing into this table with `POST /v1/tables/{tableId}/imports`. `PUT` the file's bytes to `uploadUrl` before `expiresAt` (two hours). Uploading changes nothing in the table. Files up to 50 MB. A workspace can ask for 20 upload addresses an hour; the 21st answers 429. A file never imported is deleted after seven days. Requires editor access.",
        "tags": ["Imports"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "format": {
                    "type": "string",
                    "enum": ["csv", "ndjson"],
                    "default": "csv",
                    "description": "`csv`, or `ndjson`: one JSON object per line, whose keys are its columns."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "An upload address",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "fileId": {
                          "type": "string",
                          "description": "Pass this to `POST /v1/tables/{tableId}/imports`."
                        },
                        "format": {
                          "type": "string",
                          "enum": ["csv", "ndjson"]
                        },
                        "uploadUrl": {
                          "type": "string",
                          "format": "uri",
                          "description": "`PUT` the file here. It works once."
                        },
                        "method": {
                          "type": "string",
                          "enum": ["PUT"]
                        },
                        "expiresAt": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/tables/{tableId}/imports": {
      "post": {
        "operationId": "createImport",
        "summary": "Import a file",
        "description": "Import a file uploaded to an address from `POST /v1/tables/{tableId}/imports/upload-url` into this table. Answers 202 at once with a `jobId`; poll `GET /v1/jobs/{jobId}` for progress. It is the app's own import: the table's row limit and auto-dedupe apply. Importing starts no enrichment and costs nothing. Without `mapping`, each key in the file goes to the column with that name or id, and a key that matches no column gets a new one, or is skipped with `createColumnsFromKeys: false`; a name that two columns share is never guessed between. With `mapping`, only the keys it lists are imported. A `fileId` imports once: the file is deleted when its import ends, whether it completes or fails. An upload that is never imported is deleted after seven days. Files up to 50 MB; a larger one fails the job, naming the size. A workspace can ask for 20 upload addresses and start 20 imports an hour; the 21st answers 429. Requires editor access.",
        "tags": ["Imports"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["fileId"],
                "properties": {
                  "fileId": {
                    "type": "string",
                    "description": "The `fileId` the upload address came with."
                  },
                  "mapping": {
                    "type": "array",
                    "maxItems": 500,
                    "description": "Which column each key goes to. A key with `column` goes to that column, named by id or by name; a name two columns share is refused. A key with `action` gets a new column (`create`) or is left out (`skip`).",
                    "items": {
                      "oneOf": [
                        {
                          "type": "object",
                          "required": ["key", "column"],
                          "properties": {
                            "key": {
                              "type": "string"
                            },
                            "column": {
                              "type": "string"
                            }
                          },
                          "additionalProperties": false
                        },
                        {
                          "type": "object",
                          "required": ["key", "action"],
                          "properties": {
                            "key": {
                              "type": "string"
                            },
                            "action": {
                              "type": "string",
                              "enum": ["create", "skip"]
                            }
                          },
                          "additionalProperties": false
                        }
                      ]
                    }
                  },
                  "hasHeaderRow": {
                    "type": "boolean",
                    "default": true,
                    "description": "CSV only: `false` when the first line is data rather than column names; the columns are then called `Column 0`, `Column 1`, \u2026"
                  },
                  "createColumnsFromKeys": {
                    "type": "boolean",
                    "default": true,
                    "description": "Without `mapping`: `false` skips keys that match no column instead of creating one."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "202": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Import started",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "jobId": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": ["queued"]
                        },
                        "format": {
                          "type": "string",
                          "enum": ["csv", "ndjson"]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/jobs/{jobId}": {
      "get": {
        "operationId": "getJob",
        "summary": "Get a job",
        "description": "The progress of a file import, by the `jobId` `POST /v1/tables/{tableId}/imports` returned. A job is kept for a week; after that, or for another workspace's job, it is not found.",
        "tags": ["Imports"],
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "The job's progress",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "jobId": {
                          "type": "string"
                        },
                        "kind": {
                          "type": "string",
                          "enum": ["import"]
                        },
                        "tableId": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "status": {
                          "type": "string",
                          "enum": ["queued", "running", "complete", "failed"]
                        },
                        "format": {
                          "type": "string",
                          "enum": ["csv", "ndjson"]
                        },
                        "processed": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Rows read from the file so far."
                        },
                        "total": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Rows in the file, once known."
                        },
                        "inserted": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Rows the table gained. `null` until complete; fewer than the file when auto-dedupe skipped some or the row limit cut it short."
                        },
                        "limitedByRowLimit": {
                          "type": "boolean",
                          "description": "True when the table's row limit stopped the import before the end of the file."
                        },
                        "error": {
                          "type": "string",
                          "nullable": true,
                          "description": "Why it failed: the reason itself when it is the caller's to fix, such as a line that is not JSON or a column that no longer exists, and `The import failed.` otherwise."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/tables/{tableId}/schedules": {
      "get": {
        "operationId": "listSchedules",
        "summary": "List schedules",
        "description": "The table's column schedules, paused ones included. Times are UTC, as stored. Requires member access.",
        "tags": ["Schedules"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "responses": {
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "The table's schedules",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "schedules": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "tableId": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "target": {
                                "type": "string",
                                "enum": ["columns", "flow"],
                                "description": "`flow` runs the table's whole flow, set up in the app; its `columnIds` are not used."
                              },
                              "columnIds": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "cadence": {
                                "type": "string",
                                "enum": ["daily", "weekly", "monthly", "interval"]
                              },
                              "time": {
                                "type": "string",
                                "description": "`HH:MM`, UTC."
                              },
                              "timezone": {
                                "type": "string",
                                "enum": ["UTC"]
                              },
                              "dayOfWeek": {
                                "type": "integer",
                                "minimum": 0,
                                "maximum": 6,
                                "description": "Weekly: 0 (Sunday) to 6 (Saturday)."
                              },
                              "dayOfMonth": {
                                "type": "integer",
                                "minimum": 1,
                                "maximum": 28,
                                "description": "Monthly: 1 to 28, so every month has it."
                              },
                              "every": {
                                "type": "object",
                                "required": ["count", "unit"],
                                "description": "Interval: how often. At least one hour.",
                                "properties": {
                                  "count": {
                                    "type": "integer",
                                    "minimum": 1
                                  },
                                  "unit": {
                                    "type": "string",
                                    "enum": ["hours", "days", "weeks"]
                                  }
                                }
                              },
                              "startsAt": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "description": "Interval: the first run."
                              },
                              "cells": {
                                "type": "string",
                                "enum": ["empty", "error", "unsatisfied", "all"],
                                "default": "all",
                                "description": "Which cells each firing runs."
                              },
                              "force": {
                                "type": "boolean"
                              },
                              "rerunDependents": {
                                "type": "boolean"
                              },
                              "paused": {
                                "type": "boolean"
                              },
                              "pausedReason": {
                                "type": "string",
                                "enum": ["key_revoked", "key_expired"],
                                "nullable": true,
                                "description": "Why the schedule is paused when the system paused it: the API key that made it was revoked, or expired before a firing. `null` for a schedule paused by hand, and while it runs. Resuming clears it."
                              },
                              "lastRunAt": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "nextRunAt": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "createdAt": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "updatedAt": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "createSchedule",
        "summary": "Create a schedule",
        "description": "Run columns on a schedule. A schedule spends from the wallet every time it fires, with nobody present, until it is paused or deleted. A table can have 10 active schedules, and a workspace can create 20 an hour (429 after). A key created with spending turned off is refused with 403 `spend_not_allowed`. A schedule made with an API key records which key, so it can be traced after the key is revoked. Every column and the whole timing are checked before anything is saved. It is stored as the app's schedule editor stores it, so it can be opened and changed there. Requires editor access.",
        "tags": ["Schedules"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["columns", "cadence", "time"],
                "additionalProperties": false,
                "properties": {
                  "columns": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 50,
                    "items": {
                      "type": "string"
                    },
                    "description": "Column ids, or names no other column shares. Each must be an AI, API or task column that is set up to run."
                  },
                  "cadence": {
                    "type": "string",
                    "enum": ["daily", "weekly", "monthly", "interval"]
                  },
                  "time": {
                    "type": "string",
                    "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
                    "description": "`HH:MM` on a 24-hour clock, in `timezone`. For `interval`, the time of the first run."
                  },
                  "timezone": {
                    "type": "string",
                    "default": "UTC",
                    "description": "IANA zone `time` is in, e.g. `Europe/Berlin`. Converted to UTC at its current offset, as the app does; a daylight-saving change then moves the run by an hour."
                  },
                  "dayOfWeek": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 6,
                    "description": "Weekly: 0 (Sunday) to 6 (Saturday)."
                  },
                  "dayOfMonth": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 28,
                    "description": "Monthly: 1 to 28, so every month has it."
                  },
                  "every": {
                    "type": "object",
                    "required": ["count", "unit"],
                    "description": "Interval: how often. At least one hour.",
                    "properties": {
                      "count": {
                        "type": "integer",
                        "minimum": 1
                      },
                      "unit": {
                        "type": "string",
                        "enum": ["hours", "days", "weeks"]
                      }
                    }
                  },
                  "cells": {
                    "type": "string",
                    "enum": ["empty", "error", "unsatisfied", "all"],
                    "default": "all",
                    "description": "Which cells each firing runs."
                  },
                  "force": {
                    "type": "boolean",
                    "default": true,
                    "description": "Rerun cells that already hold a value."
                  },
                  "rerunDependents": {
                    "type": "boolean",
                    "default": false,
                    "description": "Also rerun the columns that read these ones."
                  },
                  "paused": {
                    "type": "boolean",
                    "default": false,
                    "description": "`true` saves the schedule without letting it fire."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "201": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Schedule created",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "schedule": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "tableId": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "target": {
                              "type": "string",
                              "enum": ["columns", "flow"],
                              "description": "`flow` runs the table's whole flow, set up in the app; its `columnIds` are not used."
                            },
                            "columnIds": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            },
                            "cadence": {
                              "type": "string",
                              "enum": ["daily", "weekly", "monthly", "interval"]
                            },
                            "time": {
                              "type": "string",
                              "description": "`HH:MM`, UTC."
                            },
                            "timezone": {
                              "type": "string",
                              "enum": ["UTC"]
                            },
                            "dayOfWeek": {
                              "type": "integer",
                              "minimum": 0,
                              "maximum": 6,
                              "description": "Weekly: 0 (Sunday) to 6 (Saturday)."
                            },
                            "dayOfMonth": {
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 28,
                              "description": "Monthly: 1 to 28, so every month has it."
                            },
                            "every": {
                              "type": "object",
                              "required": ["count", "unit"],
                              "description": "Interval: how often. At least one hour.",
                              "properties": {
                                "count": {
                                  "type": "integer",
                                  "minimum": 1
                                },
                                "unit": {
                                  "type": "string",
                                  "enum": ["hours", "days", "weeks"]
                                }
                              }
                            },
                            "startsAt": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "description": "Interval: the first run."
                            },
                            "cells": {
                              "type": "string",
                              "enum": ["empty", "error", "unsatisfied", "all"],
                              "default": "all",
                              "description": "Which cells each firing runs."
                            },
                            "force": {
                              "type": "boolean"
                            },
                            "rerunDependents": {
                              "type": "boolean"
                            },
                            "paused": {
                              "type": "boolean"
                            },
                            "pausedReason": {
                              "type": "string",
                              "enum": ["key_revoked", "key_expired"],
                              "nullable": true,
                              "description": "Why the schedule is paused when the system paused it: the API key that made it was revoked, or expired before a firing. `null` for a schedule paused by hand, and while it runs. Resuming clears it."
                            },
                            "lastRunAt": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            },
                            "nextRunAt": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            },
                            "createdAt": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "updatedAt": {
                              "type": "string",
                              "format": "date-time"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/tables/{tableId}/schedules/{scheduleId}": {
      "patch": {
        "operationId": "updateSchedule",
        "summary": "Update a schedule",
        "description": "Change, pause or resume a schedule. Only the fields sent change, except the timing: send any of `cadence`, `time`, `timezone`, `dayOfWeek`, `dayOfMonth` or `every`, and send `cadence` and `time` with it, because the timing is replaced whole. `paused: true` takes it off the scheduler so it cannot fire. Resuming one counts against the table's 10 active schedules. A key created with spending turned off may only pause: any other change answers 403 `spend_not_allowed`. Requires editor access.",
        "tags": ["Schedules"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "name": "scheduleId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "columns": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 50,
                    "items": {
                      "type": "string"
                    },
                    "description": "Column ids, or names no other column shares. Each must be an AI, API or task column that is set up to run."
                  },
                  "cadence": {
                    "type": "string",
                    "enum": ["daily", "weekly", "monthly", "interval"]
                  },
                  "time": {
                    "type": "string",
                    "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
                    "description": "`HH:MM` on a 24-hour clock, in `timezone`. For `interval`, the time of the first run."
                  },
                  "timezone": {
                    "type": "string",
                    "default": "UTC",
                    "description": "IANA zone `time` is in, e.g. `Europe/Berlin`. Converted to UTC at its current offset, as the app does; a daylight-saving change then moves the run by an hour."
                  },
                  "dayOfWeek": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 6,
                    "description": "Weekly: 0 (Sunday) to 6 (Saturday)."
                  },
                  "dayOfMonth": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 28,
                    "description": "Monthly: 1 to 28, so every month has it."
                  },
                  "every": {
                    "type": "object",
                    "required": ["count", "unit"],
                    "description": "Interval: how often. At least one hour.",
                    "properties": {
                      "count": {
                        "type": "integer",
                        "minimum": 1
                      },
                      "unit": {
                        "type": "string",
                        "enum": ["hours", "days", "weeks"]
                      }
                    }
                  },
                  "cells": {
                    "type": "string",
                    "enum": ["empty", "error", "unsatisfied", "all"],
                    "default": "all",
                    "description": "Which cells each firing runs."
                  },
                  "force": {
                    "type": "boolean",
                    "default": true,
                    "description": "Rerun cells that already hold a value."
                  },
                  "rerunDependents": {
                    "type": "boolean",
                    "default": false,
                    "description": "Also rerun the columns that read these ones."
                  },
                  "paused": {
                    "type": "boolean",
                    "default": false,
                    "description": "`true` saves the schedule without letting it fire."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Schedule updated",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "schedule": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "tableId": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "target": {
                              "type": "string",
                              "enum": ["columns", "flow"],
                              "description": "`flow` runs the table's whole flow, set up in the app; its `columnIds` are not used."
                            },
                            "columnIds": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            },
                            "cadence": {
                              "type": "string",
                              "enum": ["daily", "weekly", "monthly", "interval"]
                            },
                            "time": {
                              "type": "string",
                              "description": "`HH:MM`, UTC."
                            },
                            "timezone": {
                              "type": "string",
                              "enum": ["UTC"]
                            },
                            "dayOfWeek": {
                              "type": "integer",
                              "minimum": 0,
                              "maximum": 6,
                              "description": "Weekly: 0 (Sunday) to 6 (Saturday)."
                            },
                            "dayOfMonth": {
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 28,
                              "description": "Monthly: 1 to 28, so every month has it."
                            },
                            "every": {
                              "type": "object",
                              "required": ["count", "unit"],
                              "description": "Interval: how often. At least one hour.",
                              "properties": {
                                "count": {
                                  "type": "integer",
                                  "minimum": 1
                                },
                                "unit": {
                                  "type": "string",
                                  "enum": ["hours", "days", "weeks"]
                                }
                              }
                            },
                            "startsAt": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "description": "Interval: the first run."
                            },
                            "cells": {
                              "type": "string",
                              "enum": ["empty", "error", "unsatisfied", "all"],
                              "default": "all",
                              "description": "Which cells each firing runs."
                            },
                            "force": {
                              "type": "boolean"
                            },
                            "rerunDependents": {
                              "type": "boolean"
                            },
                            "paused": {
                              "type": "boolean"
                            },
                            "pausedReason": {
                              "type": "string",
                              "enum": ["key_revoked", "key_expired"],
                              "nullable": true,
                              "description": "Why the schedule is paused when the system paused it: the API key that made it was revoked, or expired before a firing. `null` for a schedule paused by hand, and while it runs. Resuming clears it."
                            },
                            "lastRunAt": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            },
                            "nextRunAt": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            },
                            "createdAt": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "updatedAt": {
                              "type": "string",
                              "format": "date-time"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "deleteSchedule",
        "summary": "Delete a schedule",
        "description": "Delete a schedule so it never fires again; from then on it reads as not found. A run it already started is not stopped: `POST /v1/tables/{tableId}/stop` does that. Requires editor access.",
        "tags": ["Schedules"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "name": "scheduleId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Schedule deleted",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "deleted": {
                          "type": "boolean",
                          "enum": [true]
                        },
                        "scheduleId": {
                          "type": "string",
                          "format": "uuid"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/tables/{tableId}/exports": {
      "post": {
        "operationId": "createExport",
        "summary": "Export a table",
        "description": "Export a table, or one of its views, as CSV or JSON lines. Answers 202 at once with an `exportId`; poll `GET /v1/exports/{exportId}` for the download link. The file is the one the app's own export produces: the same rows, the same columns, references resolved. A workspace can start 20 exports an hour; the 21st answers 429. Requires member access.",
        "tags": ["Exports"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "viewId": {
                    "type": "string",
                    "description": "Export what this view shows: its filters, sort and visible columns. Omit to export the whole table."
                  },
                  "format": {
                    "type": "string",
                    "enum": ["csv", "jsonl"],
                    "default": "csv",
                    "description": "`csv`, as the app exports, or `jsonl`: one JSON object per row, keyed by column header, with the row's `_row_id`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "202": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Export started",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "exportId": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": ["queued"]
                        },
                        "format": {
                          "type": "string",
                          "enum": ["csv", "jsonl"]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/exports/{exportId}": {
      "get": {
        "operationId": "getExport",
        "summary": "Get an export",
        "description": "The progress of an export and, once it is complete, a short-lived download link. Another workspace's export, or one whose file has been deleted, is not found.",
        "tags": ["Exports"],
        "parameters": [
          {
            "name": "exportId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The id `POST /v1/tables/{tableId}/exports` returned."
          }
        ],
        "responses": {
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "The export's status",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "exportId": {
                          "type": "string",
                          "description": "The export's id, from `POST /v1/tables/{tableId}/exports`."
                        },
                        "tableId": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "status": {
                          "type": "string",
                          "enum": ["queued", "running", "complete", "failed"]
                        },
                        "format": {
                          "type": "string",
                          "enum": ["csv", "jsonl"]
                        },
                        "rowCount": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Rows in the file. `null` until the export is complete."
                        },
                        "resultUrl": {
                          "type": "string",
                          "format": "uri",
                          "nullable": true,
                          "description": "A download link to the file, signed on this request. `null` until the export is complete. Each poll returns a fresh link; it works until `expiresAt`."
                        },
                        "expiresAt": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "When `resultUrl` stops working: an hour from this request, or when the file itself is deleted (seven days after it was made), whichever is sooner."
                        },
                        "error": {
                          "type": "string",
                          "nullable": true,
                          "description": "Why the export failed, when it did."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/tables": {
      "get": {
        "operationId": "listProjects",
        "summary": "List tables",
        "description": "List the tables of the authenticated team, newest first. Optionally filtered by name or folder, and paged with `pageSize` and `cursor`.",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Only tables whose name contains this, ignoring case.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "folderId",
            "in": "query",
            "required": false,
            "description": "Only tables in this folder.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "description": "Tables per page, newest first. Omit it for every matching table in one answer, as before. A value outside 1 to 500 is refused with 400.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The `nextCursor` of the previous page, unchanged. Pages are cut on creation time and id, so a table created between calls neither repeats nor skips a row. A cursor this endpoint did not return is refused with 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "tags": ["Tables"],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Tables retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "tables": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ProjectSummary"
                          }
                        },
                        "nextCursor": {
                          "type": "string",
                          "nullable": true,
                          "description": "Pass as `cursor` for the next page. `null` when nothing follows, and always `null` without `pageSize`."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "operationId": "createProject",
        "summary": "Create a table",
        "description": "Create a new table. Supports various starter types including blank, google-search, google-maps, instagram-search, linkedin-reactions, meta-ads, apify-scrape, luma-event, hubspot-import, salesforce-import, heyreach-import, and webhook. `blank` and `webhook` are free: they create an empty table and spend nothing. A paid starter holds its worst-case cost before it runs and is charged what it really cost, never more. One the wallet cannot cover is refused with 402 `insufficient_balance` before the table is created. The search type imports a Search result set by its searchId (input.searchId, input.limit?) into a new table, free, through the app's own import and its refusals; a large import answers with run.status running and keeps filling. A starter or import refused for a reason on Loopster's side (a source's account unavailable) answers 503 \"This import is temporarily unavailable. Please try again later.\": the request is fine, so send it again later rather than changing it.",
        "tags": ["Tables"],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "Table title"
                  },
                  "type": {
                    "type": "string",
                    "description": "Starter type. Defaults to 'blank'.",
                    "default": "blank",
                    "enum": [
                      "blank",
                      "google-search",
                      "google-maps",
                      "instagram-search",
                      "linkedin-reactions",
                      "meta-ads",
                      "apify-scrape",
                      "luma-event",
                      "hubspot-import",
                      "salesforce-import",
                      "heyreach-import",
                      "webhook",
                      "search"
                    ]
                  },
                  "input": {
                    "type": "object",
                    "description": "Starter-specific input. Required for non-blank starters. For type search: { searchId (from a Search answer), limit? }."
                  },
                  "autoplay": {
                    "type": "boolean",
                    "default": true,
                    "description": "Whether inserted rows run the table's enrichment columns automatically. Defaults to true. Taken by blank and webhook tables; a starter type refuses it with 400, so create the table and then PATCH /v1/tables/{tableId}. Anything other than true or false is refused with 400."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              },
              "X-RateLimit-Spend-Limit": {
                "$ref": "#/components/headers/RateLimitSpendLimit"
              },
              "X-RateLimit-Spend-Remaining": {
                "$ref": "#/components/headers/RateLimitSpendRemaining"
              }
            },
            "description": "Table created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "enrichment": {
                          "type": "string",
                          "enum": ["started", "none", "not_started", "unknown"],
                          "description": "For a starter that runs its first rows (`google-search`, `google-maps`, `instagram-search`, `linkedin-reactions`): whether those rows started. `started`: handed over. `none`: none were meant to run, because `runFirst10` was false or the source returned nothing to run. `not_started`: they were meant to start and Loopster could not hand them to its workers at all; `enrichmentMessage` and `rowsToTrigger` are always present with it. `unknown`: sent and not confirmed; see `enrichmentMessage`. The table is created and its source charged in every case. Absent for every other type. When `enrichmentMessage` says the fault is on Loopster's side, triggering the rows now will likely fail the same way; try later."
                        },
                        "enrichmentMessage": {
                          "type": "string",
                          "description": "Present only when the first rows were meant to start and were not confirmed: what happened and what to do."
                        },
                        "rowsToTrigger": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Present only when the first rows could not be handed over: their ids, to send to Trigger Rows."
                        },
                        "table": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "title": {
                              "type": "string"
                            },
                            "type": {
                              "type": "string"
                            },
                            "jobId": {
                              "type": "string",
                              "description": "Only returned for apify-scrape type."
                            },
                            "autoplay": {
                              "type": "boolean",
                              "description": "The Auto-run setting the table was created with. Returned for blank and webhook tables."
                            }
                          }
                        },
                        "ignoredKeys": {
                          "$ref": "#/components/schemas/IgnoredKeys"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "description": "`type: \"search\"` only: no search with this `searchId` in this workspace. A `searchId` lasts about 24 hours and a workspace keeps its 1,000 most recent; run the search again for a new one. A table type that does not exist, or is not available, is a `400` naming the valid types, not a `404`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/NoResults"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "The import is temporarily unavailable for a reason on Loopster's side. The request itself is fine: send it again later rather than changing it. Three answers: \"This import is temporarily unavailable. Please try again later.\", with no Retry-After, when a source's account is unavailable; \"<Source> is temporarily unavailable while we resolve an incident. Please try again shortly.\", with a Retry-After in seconds, when Loopster has paused the source's provider during an incident; or \"<Source> is unavailable right now. Please try again later.\", with a Retry-After in seconds, when Loopster's own account with the source's provider has nothing left. None starts anything, so an Idempotency-Key does not keep it: retry with the same key. A pause or a used-up account met after a google-maps search had already paid for part of its grid keeps the key instead: send the request again with a new one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          }
        }
      }
    },
    "/v1/tables/{tableId}": {
      "get": {
        "operationId": "getProject",
        "summary": "Get a table",
        "description": "Get table details including column definitions.",
        "tags": ["Tables"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Table retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "table": {
                          "$ref": "#/components/schemas/ProjectDetail"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "updateProject",
        "summary": "Update a table",
        "description": "Update a table's title and settings: autoplay, createColumnsFromKeys and autoDedupe. apiEnabled is read-only here: false is refused with 400, because it would refuse every later request to the table, this key's included; true is accepted as a no-op.",
        "tags": ["Tables"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 256,
                    "description": "Table name. Stored as varchar(256); null, empty and longer values are rejected with 400."
                  },
                  "autoplay": {
                    "type": "boolean"
                  },
                  "createColumnsFromKeys": {
                    "type": "boolean",
                    "description": "Whether a row insert creates a manual column for each key that matches no column. Off by default; unmatched keys are then reported in ignoredKeys."
                  },
                  "apiEnabled": {
                    "type": "boolean",
                    "description": "Read-only. false is refused with 400 (change it in the app); true changes nothing, so a value read from Get Table can be sent back."
                  },
                  "autoDedupe": {
                    "type": "object",
                    "additionalProperties": false,
                    "minProperties": 1,
                    "description": "Auto-dedupe from now on. A row that is inserted (by the API, the table's webhook or the app), or whose columnId cell is edited, is a duplicate when its value there matches another row's, ignoring case and surrounding spaces. keep oldest (the default) keeps the row created first: a duplicate being inserted is not added, and an edited one is deleted. keep newest keeps the latest and deletes the earlier rows. Blank values and values longer than 200 characters never match. Only the keys sent change. Turning it on needs a columnId, sent now or already set, of a column in this table; otherwise 400 and nothing is written. Rows already in the table are not deduplicated until one is inserted or edited. An unknown key is refused with 400.",
                    "properties": {
                      "enabled": { "type": "boolean" },
                      "columnId": { "type": "string", "description": "The column whose values identify a duplicate row." },
                      "keep": {
                        "type": "string",
                        "enum": ["oldest", "newest"],
                        "description": "Which row survives a match: oldest (the default) or newest."
                      }
                    }
                  }
                },
                "minProperties": 1
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Table updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "table": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "title": {
                              "type": "string"
                            },
                            "autoplay": {
                              "type": "boolean"
                            },
                            "settings": {
                              "$ref": "#/components/schemas/TableSettings"
                            }
                          }
                        },
                        "note": {
                          "type": "string",
                          "description": "Present when the body also sent folderId or workbookId, which are never applied here: where a table is filed (POST /v1/workbooks/{workbookId}/tables)."
                        },
                        "ignoredKeys": {
                          "$ref": "#/components/schemas/IgnoredKeys"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "operationId": "deleteTable",
        "summary": "Delete a table",
        "description": "Move a table to Trash. The table stops appearing in listings and is no longer reachable through the API, but its rows and cells are kept and it can be restored from Trash for 30 days, after which it is permanently purged. A table that is already in Trash answers 404.",
        "tags": ["Tables"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Table moved to Trash",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "table": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "deleted": {
                              "type": "boolean"
                            },
                            "deletedAt": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "description": "When the table entered Trash. Null when another request trashed it first."
                            },
                            "purgesAt": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "description": "When the table stops being recoverable from Trash (List Trash, Restore from Trash). Null when another request trashed it first."
                            }
                          }
                        },
                        "alreadyDeleted": {
                          "type": "boolean",
                          "description": "True when a concurrent request had already moved this table to Trash."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/webhook-endpoints": {
      "get": {
        "operationId": "listWebhookEndpoints",
        "summary": "List webhook endpoints",
        "description": "This workspace's outbound webhook endpoints. Each shows its host and a short hint of the URL (scheme, host, at most the last four characters of the path), never the whole URL, which for most receivers is a credential, and never its signing secret. Member key.",
        "tags": ["Webhooks"],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "X-RateLimit-Scope": { "$ref": "#/components/headers/RateLimitScope" }
            },
            "description": "Webhook endpoints",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessResponse" },
                    {
                      "type": "object",
                      "properties": { "endpoints": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookEndpoint" } } }
                    }
                  ]
                }
              }
            }
          },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      },
      "post": {
        "operationId": "createWebhookEndpoint",
        "summary": "Create a webhook endpoint",
        "description": "Register an https URL for Loopster to POST to when one of this workspace's runs settles. The answer carries the full `url` and the signing `secret` **once**; neither is shown again. Editor key with a recorded creator (a key with none gets 403 `legacy_key`). Up to 10 endpoints per workspace (409 `endpoint_limit`); 30 creates and deletes an hour per workspace (429 `rate_limited`). The URL must be https to a public host by name, on port 443 or 1024–65535, with no IP address and no `user:password@`; a refusal never repeats it.",
        "tags": ["Webhooks"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url"],
                "properties": {
                  "url": { "type": "string", "maxLength": 2048, "description": "The https URL to deliver to." },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": { "type": "string", "enum": ["run.settled"] },
                    "description": "Which events to send. Only `run.settled` exists today, and it is the default."
                  },
                  "description": { "type": "string", "maxLength": 500, "nullable": true }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "X-RateLimit-Scope": { "$ref": "#/components/headers/RateLimitScope" }
            },
            "description": "Endpoint created",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessResponse" },
                    {
                      "type": "object",
                      "properties": {
                        "endpoint": { "$ref": "#/components/schemas/WebhookEndpoint" },
                        "url": { "type": "string", "description": "The full URL, shown only here." },
                        "secret": {
                          "type": "string",
                          "description": "The signing secret, `whsec_…`, shown only here. Verify each delivery's `Loopster-Signature` with it."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "description": "The workspace already has 10 endpoints (`code: endpoint_limit`)." },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/webhook-endpoints/{endpointId}": {
      "delete": {
        "operationId": "deleteWebhookEndpoint",
        "summary": "Delete a webhook endpoint",
        "description": "Remove an endpoint and its delivery log; nothing more is sent to it. Editor key with a recorded creator. A second delete answers 404.",
        "tags": ["Webhooks"],
        "parameters": [
          {
            "name": "endpointId",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "The endpoint id, from List webhook endpoints or Create a webhook endpoint."
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "X-RateLimit-Scope": { "$ref": "#/components/headers/RateLimitScope" }
            },
            "description": "Endpoint removed",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessResponse" },
                    { "type": "object", "properties": { "removed": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" } } }
                  ]
                }
              }
            }
          },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/webhook-endpoints/{endpointId}/test": {
      "post": {
        "operationId": "testWebhookEndpoint",
        "summary": "Test a webhook endpoint",
        "description": "Send one signed `ping` delivery now and answer what came back: whether it was delivered, the response status, content type, size and duration, or an error class. Never the response text and never the URL. Redirects are not followed: a 3xx is a failure, so register the final URL. Editor key; 10 tests an hour per endpoint (429 `test_limited`); 502 `delivery_unavailable` when the test could not be started, in which case nothing was sent.",
        "tags": ["Webhooks"],
        "parameters": [
          {
            "name": "endpointId",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "The endpoint id, from List webhook endpoints or Create a webhook endpoint."
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "X-RateLimit-Scope": { "$ref": "#/components/headers/RateLimitScope" }
            },
            "description": "What came back",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessResponse" },
                    { "type": "object", "properties": { "delivery": { "$ref": "#/components/schemas/WebhookDelivery" } } }
                  ]
                }
              }
            }
          },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "502": { "description": "The test could not be started; nothing was sent (`code: delivery_unavailable`)." }
        }
      }
    },
    "/v1/webhook-endpoints/{endpointId}/deliveries": {
      "get": {
        "operationId": "listWebhookDeliveries",
        "summary": "List webhook deliveries",
        "description": "The endpoint's delivery log, newest first, kept 30 days: each delivery's event, run, status, attempt, and what came back (response status, content type, size, duration, error class). Never a request or response body, and never the URL. Member key. Page with `nextCursor`; a cursor from another endpoint's log is refused.",
        "tags": ["Webhooks"],
        "parameters": [
          {
            "name": "endpointId",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "The endpoint id, from List webhook endpoints or Create a webhook endpoint."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": { "type": "string" },
            "description": "`nextCursor` from the previous page."
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "X-RateLimit-Scope": { "$ref": "#/components/headers/RateLimitScope" }
            },
            "description": "Deliveries",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessResponse" },
                    {
                      "type": "object",
                      "properties": {
                        "deliveries": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookDeliveryLogEntry" } },
                        "nextCursor": { "type": "string", "nullable": true }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/webhook-endpoints/{endpointId}/rotate-secret": {
      "post": {
        "operationId": "rotateWebhookSecret",
        "summary": "Rotate a webhook signing secret",
        "description": "A new signing secret, answered **once**. The old one keeps signing beside it until `previousSecretExpiresAt`, 24 hours on (each delivery carries a `v1=` for both), so the receiver can switch without dropping a delivery. A second rotation within that window drops the oldest. Editor key with a recorded creator, who is recorded with the rotation; counts toward the 30 changes an hour per workspace. Not offered as an MCP tool, so a new secret never passes through a model's context.",
        "tags": ["Webhooks"],
        "parameters": [
          {
            "name": "endpointId",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "The endpoint id, from List webhook endpoints or Create a webhook endpoint."
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "X-RateLimit-Scope": { "$ref": "#/components/headers/RateLimitScope" }
            },
            "description": "Secret rotated",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessResponse" },
                    {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string", "format": "uuid" },
                        "secret": { "type": "string", "description": "The new signing secret, `whsec_…`, shown only here." },
                        "previousSecretExpiresAt": { "type": "string", "format": "date-time" }
                      }
                    }
                  ]
                }
              }
            }
          },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/trash": {
      "get": {
        "operationId": "listTrash",
        "summary": "List Trash",
        "description": "What this workspace's Trash holds and can still restore: tables deleted on their own, workbooks (tableCount: the tables that went down with them) and functions, newest first, each with purgesAt. The app's Trash page reads the same list. A table that went down with its workbook is not listed on its own, and an item past purgesAt is not listed. Nor is a table whose API is switched off; its restore answers 404 too.",
        "tags": ["Projects"],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "X-RateLimit-Scope": { "$ref": "#/components/headers/RateLimitScope" }
            },
            "description": "Trash listed",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessResponse" },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": { "type": "string", "format": "uuid" },
                              "kind": { "type": "string", "enum": ["table", "workbook", "function"] },
                              "name": { "type": "string" },
                              "tableCount": {
                                "type": "integer",
                                "description": "For a workbook, the tables that come back with it. 0 otherwise."
                              },
                              "deletedAt": { "type": "string", "format": "date-time", "nullable": true },
                              "purgesAt": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "description": "When it stops being recoverable; the nightly purge deletes it after this."
                              }
                            }
                          }
                        },
                        "retentionDays": { "type": "integer" }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/trash/{itemId}/restore": {
      "post": {
        "operationId": "restoreFromTrash",
        "summary": "Restore from Trash",
        "description": "Bring back an item List Trash shows, by its id; its kind is read from the listing. A workbook comes back with its tables; a table restored alone is placed in a workbook. Nothing runs or is billed. An id not in this workspace's Trash is 404 and restores nothing. Editor key.",
        "tags": ["Projects"],
        "parameters": [
          { "name": "itemId", "in": "path", "required": true, "description": "An item id from List Trash.", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "X-RateLimit-Scope": { "$ref": "#/components/headers/RateLimitScope" }
            },
            "description": "Restored",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessResponse" },
                    {
                      "type": "object",
                      "properties": {
                        "restored": {
                          "type": "object",
                          "properties": {
                            "id": { "type": "string", "format": "uuid" },
                            "kind": { "type": "string", "enum": ["table", "workbook", "function"] },
                            "name": { "type": "string" },
                            "tableCount": {
                              "type": "integer",
                              "description": "For a workbook, the tables that come back with it. 0 otherwise."
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/folders": {
      "get": {
        "operationId": "listFolders",
        "summary": "List folders",
        "description": "This workspace's folders, by name. A folder holds workbooks; List Workbooks says which.",
        "tags": ["Projects"],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Folders",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "folders": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "name": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "createFolder",
        "summary": "Create a folder",
        "description": "Create an empty folder. Editor key. name is required, 1\u2013256 characters.",
        "tags": ["Projects"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 256
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Folder created",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "folder": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "name": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/workbooks": {
      "get": {
        "operationId": "listWorkbooks",
        "summary": "List workbooks",
        "description": "This workspace's live workbooks, each with its folderId (null at the top level) and its tableIds in tab order. Every table is in exactly one workbook, and its folder is that workbook's.",
        "tags": ["Projects"],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Workbooks",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "workbooks": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "name": {
                                "type": "string"
                              },
                              "folderId": {
                                "type": "string",
                                "format": "uuid",
                                "nullable": true
                              },
                              "tableIds": {
                                "type": "array",
                                "items": {
                                  "type": "string",
                                  "format": "uuid"
                                },
                                "description": "Live tables, in tab order."
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "createWorkbook",
        "summary": "Create a workbook",
        "description": "Create an empty workbook at the top level or in a folder. Editor key. A folderId that is not one of this workspace's folders is 404.",
        "tags": ["Projects"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 256,
                    "description": "Defaults to Untitled Workbook."
                  },
                  "folderId": {
                    "type": "string",
                    "format": "uuid",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Workbook created",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "workbook": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "name": {
                              "type": "string"
                            },
                            "folderId": {
                              "type": "string",
                              "format": "uuid",
                              "nullable": true
                            },
                            "tableIds": {
                              "type": "array",
                              "items": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "description": "Live tables, in tab order."
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/workbooks/{workbookId}": {
      "patch": {
        "operationId": "moveWorkbook",
        "summary": "Move a workbook",
        "description": "Move a workbook, and every table in it, into a folder; folderId null is the top level. Editor key. Another workspace's workbook or folder is 404.",
        "tags": ["Projects"],
        "parameters": [
          {
            "name": "workbookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "A workbook id from List Workbooks."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["folderId"],
                "properties": {
                  "folderId": {
                    "type": "string",
                    "format": "uuid",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Moved",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "workbook": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "folderId": {
                              "type": "string",
                              "format": "uuid",
                              "nullable": true
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/workbooks/{workbookId}/tables": {
      "post": {
        "operationId": "moveTableToWorkbook",
        "summary": "Move a table into a workbook",
        "description": "File a table in this workbook, out of the one it is in now; it takes the workbook's folder. A table's folderId cannot be set on its own (Update Table refuses it). Editor key. Another workspace's table or workbook is 404.",
        "tags": ["Projects"],
        "parameters": [
          {
            "name": "workbookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "A workbook id from List Workbooks."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["tableId"],
                "properties": {
                  "tableId": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Moved",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "table": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "workbookId": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "folderId": {
                              "type": "string",
                              "format": "uuid",
                              "nullable": true
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/tables/{tableId}/columns": {
      "get": {
        "operationId": "listColumns",
        "summary": "List columns",
        "description": "List all columns for a table, including their configuration details.",
        "tags": ["Columns"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Columns retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "columns": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Column"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "createColumn",
        "summary": "Create a column, or several",
        "description": "Create a column from a template, or several in one call. Use the list column templates endpoint to discover available templates.\n\nFor several, send `{\"columns\": [ … ]}`: 1 to 20 entries, each exactly what the single create takes. They are created in the order sent, and each is judged against the table plus the entries before it, so a template that reads a column by its header can read one made earlier in the same call. All or nothing: if any entry is refused, nothing is stored, and the error starts with `columns[i]:` and keeps that refusal's own status. The answer lists the created columns in `created`, in order. A prompt or input can read one field inside another column's result with a field reference: `@{ref:v1:<data>}`, where `<data>` is the base64url encoding, with no `=` padding, of the JSON `{\"columnId\":\"<parent column id>\",\"source\":\"value\" or \"metadata\",\"path\":[<keys and list positions>]}`, with exactly those three keys in that order and no spaces. A column built in the app returns its references in this form. The dependency is the parent column, never the reference. A reference that cannot be read, or whose parent is not a column of the table, is stored as sent and reported in `warnings`: the column cannot use it. A mention names a column by its id (@{col_…}): a header of one of this table's columns written in its place is refused with 400 naming the id, since every row would skip with \"Missing required input\"; an id the table does not have yet is stored with a warning.",
        "tags": ["Columns"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  { "$ref": "#/components/schemas/CreateColumnRequest" },
                  {
                    "type": "object",
                    "required": ["columns"],
                    "additionalProperties": false,
                    "properties": {
                      "columns": {
                        "type": "array",
                        "minItems": 1,
                        "maxItems": 20,
                        "description": "The columns to create, in order, each a single create's body. A batch takes no other top-level key.",
                        "items": { "$ref": "#/components/schemas/CreateColumnRequest" }
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Column created, or for a batch, every column created",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "childColumnsBuilt": { "$ref": "#/components/schemas/BuiltChildColumns" },
                        "column": {
                          "$ref": "#/components/schemas/Column"
                        },
                        "appliedFixes": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "warnings": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "droppedSteps": {
                          "type": "array",
                          "description": "For a waterfall: the template's steps this column does not have. Omitted when none were left out.",
                          "items": {
                            "$ref": "#/components/schemas/DroppedStep"
                          }
                        },
                        "verification": {
                          "$ref": "#/components/schemas/VerificationReport"
                        },
                        "ignoredKeys": {
                          "$ref": "#/components/schemas/IgnoredKeys"
                        },
                        "created": {
                          "type": "array",
                          "description": "Only for a batch: the created columns, in the order sent, each with the fixes applied and keys not read on its way in. A batch answers no top-level `column`; its top-level `appliedFixes` and `warnings` are table-wide and name their column.",
                          "items": {
                            "type": "object",
                            "required": ["column"],
                            "properties": {
                              "column": { "$ref": "#/components/schemas/Column" },
                              "appliedFixes": { "type": "array", "items": { "type": "string" } },
                              "ignoredKeys": { "$ref": "#/components/schemas/IgnoredKeys" }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request: an unknown template, a refused setting, or a waterfall step this workspace cannot start. The last carries code step_not_runnable and steps, each with its id, taskType, provider and waysBack (connect_key; and where Loopster offers an account for the provider, save_default and explicit_managed). A step with no account named never starts on Loopster's account on its own: set executionMode to managed on the step to run it there, billed to the wallet. A step this kind of waterfall does not offer (a provider held out of waterfalls, a finder for another kind, a verifier, a pattern guess) is refused with code step_not_offered and steps, each with its id, taskType and reason, whatever account it names. Verification switched on in the request where it has no account to run on (no key of the workspace's own, and Loopster's account not saved as the verifier's default) is refused with code verification_not_runnable and a verification object naming the verifier and the ways through. A waterfall setting that adds a column in the app's grid (outputSuccessfulProvider, outputVerification) is refused with code setting_not_available: what those columns show is on every row, in waterfall on the cell holding the answer. Nothing is created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AccountsUnavailable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tables/{tableId}/columns/templates": {
      "get": {
        "operationId": "listColumnTemplates",
        "summary": "List column templates",
        "description": "List the column templates that can be used to create new columns. Optionally filter by column type, provider and/or Add column menu tab \u2014 the unfiltered list is large, and an unrecognised filter value is answered with a 400 naming every valid one.",
        "tags": ["Columns"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Column types to return: manual, ai, api, task. Comma-separate or repeat the parameter. Omit for all types."
          },
          {
            "name": "provider",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Provider ids to return, e.g. `hubspot,instantly`. Comma-separate or repeat the parameter. Omit for all providers."
          },
          {
            "name": "tab",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Add column menu tabs to return: import, enrich, tools, functions, export. Comma-separate or repeat the parameter. Omit for every template, including column types and model variants, which have no tab."
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Templates retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "templates": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ColumnTemplate"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tables/{tableId}/columns/templates/{templateId}": {
      "get": {
        "operationId": "getColumnTemplate",
        "summary": "Get a column template",
        "description": "One column template: the entry List Column Templates gives, plus settings \u2014 the config block a column made from it is configured by (taskConfig, aiConfig or apiConfig), the template's defaults, and for a task template the JSON Schema (draft-07) of its taskConfig, generated from the schema Create Column and Update Column validate against. A key the schema does not list is refused by those writes. A manual template has no settings. Answers for every id Create Column accepts: an unknown id is 404, a template whose provider cannot be connected is 400, as on create.",
        "tags": ["Columns"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "name": "templateId",
            "in": "path",
            "required": true,
            "description": "Template id, from List Column Templates.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Template retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "template": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/ColumnTemplate"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "settings": {
                                  "type": "object",
                                  "description": "Absent for a manual template.",
                                  "properties": {
                                    "field": {
                                      "type": "string",
                                      "enum": ["taskConfig", "aiConfig", "apiConfig"],
                                      "description": "The config block a column made from this template is configured by."
                                    },
                                    "taskType": {
                                      "type": "string",
                                      "description": "Task templates only: the key the config sits under inside taskConfig."
                                    },
                                    "configSchema": {
                                      "type": "object",
                                      "additionalProperties": true,
                                      "description": "Task templates only: JSON Schema (draft-07) of taskConfig[taskType]."
                                    },
                                    "defaults": {
                                      "type": "object",
                                      "additionalProperties": true,
                                      "description": "The config the template ships with."
                                    }
                                  }
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/tables/{tableId}/columns/{columnId}": {
      "patch": {
        "operationId": "updateColumn",
        "summary": "Update a column",
        "description": "Update a column's properties such as header, type, data shape, dependencies, and configuration.\n\nEvery field is validated against the registries that define it, and a value that cannot work is rejected with 400 naming the value and the valid ones \u2014 an unknown model id, a tool that does not exist, a data shape we do not define, a task configuration the registry calls invalid, a task setting the task does not have (a key the column already carries is accepted, so a `GET` sent back unchanged still saves), and a column reference joined to an operator such as `||` in a task input, which substitutes references as text and evaluates nothing. Nothing is stored that a later run would have to fail on.\n\nThe four `*Config` objects are merged into the stored configuration rather than replacing it, so patching `{\"aiConfig\": {\"model\": \"\u2026\"}}` keeps the prompt, the tools and the tier. Set a field to `null` to remove it. Arrays replace wholesale. A body key this endpoint does not read is reported in `ignoredKeys`; a body with no readable keys at all is rejected with 400. A prompt or input can read one field inside another column's result with a field reference: `@{ref:v1:<data>}`, where `<data>` is the base64url encoding, with no `=` padding, of the JSON `{\"columnId\":\"<parent column id>\",\"source\":\"value\" or \"metadata\",\"path\":[<keys and list positions>]}`, with exactly those three keys in that order and no spaces. A column built in the app returns its references in this form. The dependency is the parent column, never the reference. A reference that cannot be read, or whose parent is not a column of the table, is stored as sent and reported in `warnings`: the column cannot use it. A mention names a column by its id (@{col_\u2026}): a header of one of this table's columns written in its place is refused with 400 naming the id, since every row would skip with \"Missing required input\"; an id the table does not have yet is stored with a warning. A request that changes `aiConfig`, `apiConfig`, `taskConfig`, `referenceConfig` or `type` and sends no `dependencies` sets the column's dependencies to what its new configuration reads, keeping any you sent that it does not read (or with a `required` it would not give) and any a person pinned in the app, and names each one it removed in `appliedFixes`: sending the configuration again repairs a column still waiting on columns it no longer reads. A request that sends `dependencies` stores that list as sent, and an entry that matches what the configuration reads still follows it; one that changes no configuration leaves them as they are.",
        "tags": ["Columns"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "$ref": "#/components/parameters/columnId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "header": {
                    "type": "string",
                    "minLength": 1
                  },
                  "type": {
                    "type": "string",
                    "enum": ["manual", "ai", "api", "task", "reference"]
                  },
                  "dataShape": {
                    "type": "string",
                    "description": "One of the data shapes the product defines; see the dataShape returned by GET /columns. An unrecognised value is rejected with 400 rather than stored. `people-table-link` is set by Loopster, on a company table's People search column, and is refused with 400 as a change; sent back as the column's current dataShape, it is accepted and changes nothing."
                  },
                  "order": {
                    "type": "number"
                  },
                  "description": {
                    "type": "string"
                  },
                  "dependencies": {
                    "type": "array",
                    "description": "Array of column IDs (strings) or objects with columnId and required fields. Without required, a column the configuration reads takes the required it reads it with, and any other is required, except on a waterfall column, where it defaults to false: a waterfall waits for that column without skipping a row that has none, as it does for its own inputs. One the column's configuration does not read, or sent with a required it would not give, is kept when the configuration later changes; one that matches what it reads follows it.",
                    "items": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "columnId": {
                              "type": "string"
                            },
                            "required": {
                              "type": "boolean"
                            }
                          },
                          "required": ["columnId"]
                        }
                      ]
                    }
                  },
                  "aiConfig": {
                    "type": "object",
                    "description": "AI column configuration (for type=ai). Merged into the stored configuration; send null for a field to remove it. `model` must be a registered model id and every entry in `tools` a registered tool, or the request is rejected with 400 naming the valid ones. Unknown fields are rejected too \u2014 a misspelt setting used to be stored and silently do nothing. `account` chooses who pays: `\"loopster\"` runs on Loopster's account and bills your wallet, `\"user\"` runs on your own OpenRouter key (choose which one with `integrationConnectionId`). `\"fluar\"`, an older name for `\"loopster\"`, is still accepted and is stored and returned as `\"loopster\"`."
                  },
                  "apiConfig": {
                    "type": "object",
                    "description": "API column configuration (for type=api). Merged into the stored configuration; send null for a field to remove it."
                  },
                  "taskConfig": {
                    "type": "object",
                    "description": "Task column configuration (for type=task), of the form { taskType, [taskType]: { \u2026 } }. Merged into the stored configuration; send null for a field to remove it. The task type and the configuration under it are both checked against the task registry, so a column that could never run is rejected here rather than failing on every row."
                  },
                  "referenceConfig": {
                    "type": "object",
                    "description": "Reference column configuration (for type=reference). Merged into the stored configuration; send null for a field to remove it."
                  },
                  "integrationConnectionId": {
                    "type": "string",
                    "format": "uuid",
                    "nullable": true,
                    "description": "Which of the workspace's connections a task column runs on: an id from GET /v1/connections, for the provider the column's task uses. Send null to clear it. Rejected with 400, naming the connections that would work, when the id is not a connected account of that provider this workspace can use, when the column is not a task column, when its task takes no connection, or when the same request sets execution_mode to \"managed\". Choosing a connection takes a column off Loopster's own account, as the column settings do, and appliedFixes says so."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Column updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "childColumnsBuilt": { "$ref": "#/components/schemas/BuiltChildColumns" },
                        "column": {
                          "$ref": "#/components/schemas/Column"
                        },
                        "appliedFixes": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "warnings": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "ignoredKeys": {
                          "$ref": "#/components/schemas/IgnoredKeys"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request: a refused setting, or a waterfall step that the workspace cannot start and that this request adds, switches on, takes the connection off, or moves to another provider or executionMode. The last carries code step_not_runnable and steps, each with its id, taskType, provider and waysBack (connect_key; and where Loopster offers an account for the provider, save_default and explicit_managed). A step with no account named never starts on Loopster's account on its own: set executionMode to managed on the step to run it there, billed to the wallet. Steps sent back as they were stored are not judged. A step this request puts into the waterfall that its kind does not offer is refused with code step_not_offered; one already there is not affected. Verification this request switches on, or moves to another verifier, where it has no account to run on is refused with code verification_not_runnable; a waterfall that already verifies stays editable. A request that would change a waterfall setting that adds a column in the app's grid (outputSuccessfulProvider, outputVerification) is refused with code setting_not_available; sent as the column has it, the setting is accepted. Nothing is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "409": {
            "description": "`waterfall_running`: this waterfall has rows running, and giving it its result column now would make those rows run, and be charged, again. Nothing was changed; send it again when they have finished. `runningCells` says how many are still running.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/ErrorResponse" },
                    {
                      "type": "object",
                      "properties": {
                        "code": { "type": "string", "enum": ["waterfall_running"] },
                        "runningCells": { "type": "integer", "description": "How many cells of the waterfall are still running." }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "operationId": "deleteColumn",
        "summary": "Delete a column",
        "description": "Delete a column and all its associated cells.",
        "tags": ["Columns"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "$ref": "#/components/parameters/columnId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Column deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "deletedColumnId": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tables/{tableId}/rows": {
      "get": {
        "operationId": "listRows",
        "summary": "List rows",
        "description": "List rows for a table, a page at a time. With `viewId`, `filter`, `sort`, `updatedSince` or `cursor`, rows are selected and ordered by the same query the grid runs and paged by cursor; otherwise by `page`.",
        "tags": ["Rows"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            },
            "description": "Page number. Out-of-range values are clamped; a value that is not a number is rejected with 400, because reading it as 'page 1' would report hasMore: false while rows remained."
          },
          {
            "name": "pageSize",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 500
            },
            "description": "Number of rows per page (max 500). Out-of-range values are clamped; a value that is not a number is rejected with 400 rather than returning the whole table."
          },
          {
            "name": "viewId",
            "in": "query",
            "required": false,
            "description": "Read the rows a saved view selects, in its order. Pages with `cursor`; `page` is refused alongside it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "A URL-encoded JSON list of conditions, the shape a view stores: `{ id, operator, value, variant }`, ANDed. `variant` is required except for `isEmpty` and `isNotEmpty`. An unknown column or operator, or a missing variant, is refused with 400. Replaces a view's own filters.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "A URL-encoded JSON list of `{ id, desc }`, outermost first. Replaces a view's own sort.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "required": false,
            "description": "Only rows updated at or after this ISO 8601 time. With a view whose conditions are joined with \"or\", it narrows the whole view rather than joining the or-list.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "columns",
            "in": "query",
            "required": false,
            "description": "Only these cells in each row, comma-separated column ids or names.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The `pagination.nextCursor` of the previous page, unchanged. Pages are anchored on the last row returned, so a row written earlier in the order is not read twice. A cursor whose row no longer matches is refused with 400: start again without it.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Rows retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "rows": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/RowSummary"
                          }
                        },
                        "pagination": {
                          "$ref": "#/components/schemas/Pagination"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "insertRows",
        "summary": "Insert rows",
        "description": "Insert one or more rows into a table. Accepts a single object or an array of objects mapping column IDs or headers to values. If the table has autoplay enabled, rows are automatically queued for processing. A record that would be stored empty is rejected with 400 rather than inserted as a blank row: one none of whose keys matches a column, on a table that neither creates columns from keys nor keeps the raw record in a trigger column. An object whose only key holds an array, such as `{\"records\": [...]}`, is rejected when the array holds rows of this table, and on a table that keeps nothing whatever it holds \u2014 send the array itself. On a table with a trigger column, a wrapper whose elements match no column is kept whole in the trigger cell, like any other unmatched record. An empty object `{}` still inserts a blank row. The request runs to completion before it answers, under a 300-second limit: about 25,000 rows fit, and an insert still running at 300 seconds is cut off. One request carries at most 25,000 rows: a larger one is refused with 413 `too_many_rows` before anything is written. Send larger loads in batches of 10,000 or fewer, or import the file through POST /v1/tables/{tableId}/imports. Add `?returnIds=false` to leave the inserted row ids out of the response.",
        "tags": ["Rows"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          },
          {
            "$ref": "#/components/parameters/onOversize"
          },
          {
            "name": "returnIds",
            "in": "query",
            "required": false,
            "schema": { "type": "boolean", "default": true },
            "description": "`false` leaves `rows` out of the response, keeping every count and partial-write field. For large inserts, where the id list is most of the payload."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "A single row as key-value pairs (column ID or header \u2192 value)."
                  },
                  {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "description": "An array of rows, each as key-value pairs. Every element must be an object; a number, a string, null or a nested array is rejected with 400 naming the index."
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              },
              "X-RateLimit-Spend-Limit": {
                "$ref": "#/components/headers/RateLimitSpendLimit"
              },
              "X-RateLimit-Spend-Remaining": {
                "$ref": "#/components/headers/RateLimitSpendRemaining"
              }
            },
            "description": "Rows inserted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "skipped": { "$ref": "#/components/schemas/SkippedRecords" },
                        "truncated": {
                          "type": "boolean",
                          "description": "Present, and true, only when the list above was cut at 1,000 entries."
                        },
                        "skippedCount": {
                          "type": "integer",
                          "description": "How many were skipped. Present with `skipped`, which lists at most 1,000 of them."
                        },
                        "insertedCount": {
                          "type": "integer"
                        },
                        "rows": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "IDs of the inserted rows. Absent when the request sets `returnIds=false`."
                        },
                        "startedEnrichment": {
                          "type": "boolean",
                          "description": "Whether the insert also started enrichment, and therefore started spending. True when the table has autoplay enabled, rows were inserted, and the table has at least one column an autoplay run executes (an AI column with its prompt, an API or task column that is not manual-trigger-only, not a formula). Always present, so a caller can check it rather than having to know the table's setting."
                        },
                        "enrichment": {
                          "type": "string",
                          "enum": ["started", "none", "not_started", "unknown"],
                          "description": "Whether the inserted rows are being enriched, in the four answers there are. Read this rather than `startedEnrichment`, which is only true or false. `started`: the run was handed over and executes at least one column, so money is moving. `none`: nothing was meant to run (autoplay off, or no column an autoplay run executes). `not_started`: the run was meant to start and Loopster could not hand it to its workers at all; `enrichmentMessage` and `rowsToTrigger` are always present with it. `unknown`: the run was sent to the workers and not confirmed, so the rows may or may not be running; see `enrichmentMessage`. Always present. The rows are stored in every case. When `enrichmentMessage` says the fault is on Loopster's side, triggering the rows now will likely fail the same way; try later."
                        },
                        "enrichmentMessage": {
                          "type": "string",
                          "description": "Present only when the run was meant to start and was not confirmed: what happened and what to do."
                        },
                        "rowsToTrigger": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Present exactly when `enrichment` is `not_started`: the ids of the rows to send to Trigger Rows. Present even with `returnIds=false`. Absent on `unknown`, where triggering without checking the table's status first could run the rows twice."
                        },
                        "columnsCreated": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Names of auto-created columns (if createColumnsFromKeys is enabled)."
                        },
                        "ignoredKeys": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Payload keys that matched no column, so no cell of their own was written. Absent when every key matched. On a table with a trigger column the full payload is still recorded in the trigger cell, so these values are retained but are not addressable as fields; on a table without one they are not stored at all. Enable createColumnsFromKeys on the table if you want unrecognised keys to become columns instead."
                        },
                        "cappedCells": {
                          "$ref": "#/components/schemas/CappedCells"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "413": {
            "description": "More rows than one request inserts. Nothing was written. `maxRows` is the cap and `received` how many this request carried; send batches, or import the file through POST /v1/tables/{tableId}/imports",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/ErrorResponse" },
                    {
                      "type": "object",
                      "properties": {
                        "code": { "type": "string", "enum": ["too_many_rows"] },
                        "maxRows": { "type": "integer", "example": 25000 },
                        "received": { "type": "integer", "example": 30000 }
                      }
                    }
                  ]
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ValueTooLarge"
          }
        }
      },
      "delete": {
        "operationId": "deleteRows",
        "summary": "Delete rows",
        "description": "Delete one or more rows by their IDs.",
        "tags": ["Rows"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["rowIds"],
                "properties": {
                  "rowIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "description": "IDs of rows to delete. Each must be a uuid; a request carrying any that is not is rejected with 400."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Rows deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "skipped": { "$ref": "#/components/schemas/SkippedRowIds" },
                        "truncated": {
                          "type": "boolean",
                          "description": "Present, and true, only when the list above was cut at 1,000 entries."
                        },
                        "skippedCount": {
                          "type": "integer",
                          "description": "How many were skipped. Present with `skipped`, which lists at most 1,000 of them."
                        },
                        "deletedCount": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tables/{tableId}/rows/{rowId}": {
      "get": {
        "operationId": "getRow",
        "summary": "Get a row",
        "description": "Get a single row with all cell data including status and error details, read exactly as List Rows reads it: cells keyed by the column ids List Columns returns, a waterfall under its own id valued from its result column (its per-step columns are not listed), and the row's status derived from its cells rather than stored. A waterfall's cell also carries `waterfall`: which provider answered, what each step did and whether the answer was verified. It is present once a run is recorded and absent on every other cell. Under an `error` or `skipped` cell it is present only when the waterfall itself ended the run with every paid step refused (the daily limit on Loopster's account, or a trial account's paid lookup); a run that failed otherwise, or that a run condition skipped, carries none, and nothing an earlier run left on the row is reported as its. The run's cost, and the value each step stood behind, are on the row's trace.",
        "tags": ["Rows"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "$ref": "#/components/parameters/rowId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Row retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "row": {
                          "$ref": "#/components/schemas/RowDetail"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "updateRow",
        "summary": "Update a row",
        "description": "Update cell values for a specific row. Keys can be column IDs or column headers. A cell a run is still computing is not written: when that leaves nothing written the answer is `409` with `code: cell_running`, and when other cells landed it is `200` with the held cells in `rejected` (`reason: running`). Send those again once the run finishes.",
        "tags": ["Rows"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "$ref": "#/components/parameters/rowId"
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          },
          {
            "$ref": "#/components/parameters/onOversize"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "description": "Key-value pairs of column ID or header \u2192 new value.",
                "minProperties": 1
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              },
              "X-RateLimit-Spend-Limit": {
                "$ref": "#/components/headers/RateLimitSpendLimit"
              },
              "X-RateLimit-Spend-Remaining": {
                "$ref": "#/components/headers/RateLimitSpendRemaining"
              }
            },
            "description": "Row updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "updatedCount": {
                          "type": "integer"
                        },
                        "rowId": {
                          "type": "string"
                        },
                        "cappedCells": {
                          "$ref": "#/components/schemas/CappedCells"
                        },
                        "rejected": { "$ref": "#/components/schemas/CellWriteRejections" },
                        "rejectedCount": {
                          "type": "integer",
                          "description": "How many cells were not written. Present with `rejected`."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "409": {
            "description": "`cell_running`: a run is still computing every cell this request writes, so nothing was saved; `rejected` lists them with `reason: running`. The Idempotency-Key is not spent, so send the same request, with the same key, once the run finishes. Also answered for an Idempotency-Key that is still in use, was used for a different request, or whose first request never finished; `code` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": ["cell_running", "idempotency_key_reused", "idempotency_key_in_progress", "idempotency_outcome_unknown"]
                        },
                        "rejected": { "$ref": "#/components/schemas/CellWriteRejections" }
                      }
                    }
                  ]
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ValueTooLarge"
          }
        }
      }
    },
    "/v1/tables/{tableId}/rows/{rowId}/trace": {
      "get": {
        "operationId": "getRowWaterfallTrace",
        "summary": "Trace a waterfall run",
        "description": "Explain what a cell did on one row \u2014 waterfall or AI. For a waterfall: every provider the chain tried, in order, what each one answered, why it did not win, and what the run cost. For an AI column: the agent's steps in order, the tools it called and what they returned, with per-step tokens and timing. Every entry carries a `kind` of `waterfall` or `agent`. Nothing new is stored and nothing is recomputed \u2014 both are read from what the run already wrote, so a trace is available for rows enriched long before this endpoint existed. An agent trace is compact by construction and carries the `limits` it was recorded under. `columnId` may name either kind. A waterfall is traced whether or not it has a column per step: one made through the API has none, its run is read from the waterfall's own cell, and its attempts carry no `columnId`. When the waterfall's result is `error` or `skipped`, steps are reported only for a run the waterfall itself ended with every paid step refused; otherwise each attempt is `skipped` with a null reason, whatever an earlier run left on the row.",
        "tags": ["Rows"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "$ref": "#/components/parameters/rowId"
          },
          {
            "name": "columnId",
            "in": "query",
            "required": false,
            "description": "Restrict the answer to one waterfall column. Omitted, every waterfall column on the row is traced. A column that is not a waterfall on this table is refused with the waterfall columns named.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Trace retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "rowId": {
                          "type": "string"
                        },
                        "traces": {
                          "type": "array",
                          "description": "Waterfall entries first, then agent entries, each discriminated by `kind`.",
                          "items": {
                            "oneOf": [
                              {
                                "$ref": "#/components/schemas/WaterfallTrace"
                              },
                              {
                                "$ref": "#/components/schemas/AgentTrace"
                              }
                            ]
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/webhooks/{secret}": {
      "post": {
        "operationId": "insertRowsViaWebhook",
        "summary": "Insert rows via webhook URL",
        "description": "Insert one or more rows into a table using its webhook URL, with **no `Authorization` header**.\n\nThe secret in the path is the credential. Get the URL from the table's Webhook & API panel by turning \"Secure with Bearer token\" off; regenerating it there invalidates the previous URL immediately. Because the secret travels in the URL \u2014 and so ends up in proxy logs, browser history and pasted messages \u2014 this endpoint only ever inserts. Reading, updating and deleting stay behind the workspace API key.\n\nThe body, the response and every limit are identical to `POST /v1/tables/{tableId}/rows`, including automatic column creation, `ignoredKeys`, `cappedCells` and autoplay queueing. Rate limits are charged to the workspace that owns the table, exactly as they are for an authenticated call.\n\nAnything that is not a live, listening table answers `404 Webhook not found` \u2014 a wrong secret, a table in Trash, and a table whose webhook listening is switched off are deliberately indistinguishable.",
        "tags": ["Rows"],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSecret"
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "A single row as key-value pairs (column ID or header \u2192 value)."
                  },
                  {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "description": "An array of rows, each as key-value pairs. Every element must be an object; a number, a string, null or a nested array is rejected with 400 naming the index."
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Rows inserted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "skipped": { "$ref": "#/components/schemas/SkippedRecords" },
                        "truncated": {
                          "type": "boolean",
                          "description": "Present, and true, only when the list above was cut at 1,000 entries."
                        },
                        "skippedCount": {
                          "type": "integer",
                          "description": "How many were skipped. Present with `skipped`, which lists at most 1,000 of them."
                        },
                        "insertedCount": {
                          "type": "integer"
                        },
                        "rows": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "IDs of the inserted rows."
                        },
                        "columnsCreated": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Names of auto-created columns (if createColumnsFromKeys is enabled)."
                        },
                        "ignoredKeys": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Payload keys that matched no column, so no cell of their own was written. Absent when every key matched. On a table with a trigger column the full payload is still recorded in the trigger cell, so these values are retained but are not addressable as fields; on a table without one they are not stored at all. Enable createColumnsFromKeys on the table if you want unrecognised keys to become columns instead."
                        },
                        "cappedCells": {
                          "$ref": "#/components/schemas/CappedCells"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "No live webhook answers at this URL. Returned for an unknown or malformed secret, a table in Trash, an address retired by turning webhook listening off, and a table whose API access is switched off \u2014 the cases are not distinguished, because this endpoint is unauthenticated and any distinction is an oracle for guessing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "405": {
            "description": "Only POST is supported on a webhook URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          }
        }
      }
    },
    "/v1/tables/{tableId}/cells": {
      "patch": {
        "operationId": "updateCells",
        "summary": "Batch update cells",
        "description": "Update multiple cells across different rows in a single request.\n\nEvery entry must name an existing column, must carry a `value` (send null to clear a cell), and each rowId/columnId pair may appear at most once per request \u2014 a repeated pair is rejected with 400 rather than failing the whole batch in the database.",
        "tags": ["Cells"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          },
          {
            "$ref": "#/components/parameters/onOversize"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["cells"],
                "properties": {
                  "cells": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "object",
                      "required": ["rowId", "columnId", "value"],
                      "properties": {
                        "rowId": {
                          "type": "string",
                          "format": "uuid",
                          "description": "Id of a row in this table. A value that is not a uuid is rejected with 400."
                        },
                        "columnId": {
                          "type": "string",
                          "description": "Id of an existing column in this table. An id that names no column is rejected with 400."
                        },
                        "value": {
                          "description": "Cell value. Objects are JSON-stringified automatically. Required: omitting it is rejected with 400, since it would otherwise clear the cell. Send null to clear one deliberately."
                        },
                        "mode": {
                          "type": "string",
                          "enum": ["set", "append"],
                          "default": "set",
                          "description": "append adds value to what the cell holds, after separator; written only if the cell has not changed since it was read, retried up to five times, else reported in rejected as stale."
                        },
                        "separator": {
                          "type": "string",
                          "maxLength": 16,
                          "description": "With mode append: between the stored value and the new one, when the stored value is not empty. None by default."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              },
              "X-RateLimit-Spend-Limit": {
                "$ref": "#/components/headers/RateLimitSpendLimit"
              },
              "X-RateLimit-Spend-Remaining": {
                "$ref": "#/components/headers/RateLimitSpendRemaining"
              }
            },
            "description": "Cells updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "rejected": { "$ref": "#/components/schemas/CellWriteRejections" },
                        "truncated": {
                          "type": "boolean",
                          "description": "Present, and true, only when the list above was cut at 1,000 entries."
                        },
                        "rejectedCount": {
                          "type": "integer",
                          "description": "How many cells were not written. Present with `rejected`, which lists at most 1,000 of them."
                        },
                        "updatedCount": {
                          "type": "integer"
                        },
                        "cappedCells": {
                          "$ref": "#/components/schemas/CappedCells"
                        },
                        "ignoredKeys": {
                          "$ref": "#/components/schemas/IgnoredKeys"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "422": {
            "$ref": "#/components/responses/ValueTooLarge"
          }
        }
      }
    },
    "/v1/tables/{tableId}/trigger": {
      "post": {
        "operationId": "triggerProcessing",
        "summary": "Trigger row processing",
        "description": "Trigger processing for rows in a table. Optionally specify which rows and columns to process. Without `rowIds` this runs EVERY row in the table and spends accordingly; send `dryRun` first. Maximum 10,000 rows per request: with no `rowIds`, a table of more than 10,000 rows is refused with 400 and nothing runs, so send `rowIds` in batches of 10,000 or fewer. With `dryRun` the same request is priced instead of run: nothing is queued and nothing is charged. Each row holds its worst-case cost before it runs and is charged what it really cost, never more. A run larger than the wallet runs rows in order while the wallet covers them; the rest are refused, not run: their cells show a not-enough-balance error and nothing is charged for them, so the wallet is never overdrawn. A run that cannot cover even its first row is refused with 402 `insufficient_balance` before anything is queued.",
        "tags": ["Processing"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "name": "dryRun",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["true"]
            },
            "description": "Set to the string `true` to price the request instead of running it: nothing is queued and nothing is charged, and the response is the estimate shape rather than the trigger shape. Also accepted as `dryRun: true` in the body, but prefer the query string \u2014 an unparseable body is treated as an empty one (which means \"trigger every row\"), so a malformed request would drop a body flag and start the run for real."
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "requestBody": {
          "description": "Optional: a request with no body at all runs every row. A body that is sent must be a JSON object; one that is not valid JSON (whitespace only included), or is JSON but not an object, is refused with 400 and nothing starts. Only `rowIds`, `columnIds`, `force`, `dryRun`, `maxCostUsd` and `allowUnpriced` are read. Any other key is reported back in `ignoredKeys` and changes nothing. With `maxCostUsd`, the run is checked against its estimate before anything starts; see the 422.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rowIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "nullable": true,
                    "description": "Row IDs to process. Defaults to all rows if omitted or null; an empty list runs nothing; any other value that is not an array is rejected with 400. Each must be a uuid; a request carrying any that is not is rejected with 400, which counts them without echoing them."
                  },
                  "columnIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Column IDs to process. Defaults to all processable columns (ai, api, task, reference) if omitted. Every id must be a column of this table: one that is not is rejected with 400, naming up to five of them and listing up to twenty of the table's columns, on the dry run and the real run alike, and nothing is priced or started. A refused request takes no slot from the credit-spend rate-limit window."
                  },
                  "force": {
                    "type": "boolean",
                    "description": "Force re-processing of already completed cells."
                  },
                  "dryRun": {
                    "type": "boolean",
                    "description": "Price the request instead of running it. Equivalent to `?dryRun=true`, which is the safer spelling because it survives a body that fails to parse."
                  },
                  "maxCostUsd": {
                    "type": "number",
                    "minimum": 0,
                    "description": "The most this run may cost, in dollars. Before anything starts, the run is estimated exactly as the dry run would estimate it; if that estimate is higher, or the run includes a column with no up-front price (unless `allowUnpriced` is true), nothing is queued and the answer is 422. It is a check at the start, not a meter: a run that starts is not stopped part-way, and AI columns are estimated from observed averages, so their actual cost can exceed the estimate. On a dry run it adds `withinCap`."
                  },
                  "allowUnpriced": {
                    "type": "boolean",
                    "description": "Only with `maxCostUsd`: start even when some columns have no up-front price. Those columns are then not checked against the cap. Refused with 400 without a cap."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              },
              "X-RateLimit-Spend-Limit": {
                "$ref": "#/components/headers/RateLimitSpendLimit"
              },
              "X-RateLimit-Spend-Remaining": {
                "$ref": "#/components/headers/RateLimitSpendRemaining"
              }
            },
            "description": "Processing triggered, or \u2014 with `dryRun` \u2014 priced without being started",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/SuccessResponse"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "ignoredKeys": { "$ref": "#/components/schemas/IgnoredKeys" },
                            "runId": {
                              "type": "string",
                              "format": "uuid",
                              "description": "The handle for this run: `GET /v1/runs/{runId}` reports on these rows only, so two triggers on one table can be told apart. Absent when nothing was dispatched, and in the rare case the run could not be recorded (then `message` says so and `GET /v1/tables/{tableId}/status` is the fallback). A replay of a keyed trigger returns the same runId."
                            },
                            "triggeredCount": {
                              "type": "integer",
                              "description": "Rows the dispatcher actually enqueued. Not the number of rows you asked for: `triggeredCount + deduplicated + rejected + unavailable` reconciles against that, so a positive count does not on its own mean everything started.",
                              "example": 8
                            },
                            "deduplicated": {
                              "type": "integer",
                              "description": "Rows that were not enqueued because a run was already in progress for them. Nothing further was queued and nothing extra will be charged for them.",
                              "example": 2
                            },
                            "rejected": {
                              "type": "integer",
                              "description": "Rows the dispatcher refused because the queue is full. They did not start and will not start on their own \u2014 trigger them again.",
                              "example": 0
                            },
                            "unavailable": {
                              "type": "integer",
                              "description": "Rows whose start could not be confirmed because the queue did not answer in time; they may or may not have started. Present only when above zero. Retrying an unforced row is safe, because a row that did start is reported as already running and counts in `deduplicated`. Retrying a forced run (`force: true`) can start it twice.",
                              "example": 1
                            },
                            "message": {
                              "type": "string",
                              "description": "Present only when some rows did not start, summarising why. Absent when every requested row was enqueued.",
                              "example": "Triggered 8 of 10 rows: 2 already had a run in progress."
                            },
                            "notInTable": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              },
                              "description": "Row ids you sent that belong to a different table. They were skipped rather than counted as started, and are absent when every id belonged. If none of the ids belong the request is refused with 400 instead.",
                              "example": ["8b1f0c2e-4c7a-4a51-9c0a-2f0d6b3a1e77"]
                            },
                            "costCap": {
                              "type": "object",
                              "description": "Present only when `maxCostUsd` was sent: the cap this run was checked against and the estimate it passed with.",
                              "properties": {
                                "maxCostUsd": {
                                  "type": "number"
                                },
                                "estimatedCostUsd": {
                                  "type": "number"
                                },
                                "unpricedColumns": {
                                  "type": "integer",
                                  "description": "Columns started without a price check (`allowUnpriced`). Present only when there were some."
                                }
                              }
                            }
                          }
                        }
                      ]
                    },
                    {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/SuccessResponse"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "ignoredKeys": { "$ref": "#/components/schemas/IgnoredKeys" },
                            "dryRun": {
                              "type": "boolean",
                              "enum": [true],
                              "description": "Always `true`. Echoed rather than implied, so a caller whose flag failed to arrive learns it here rather than from a billing statement."
                            },
                            "rows": {
                              "type": "integer",
                              "description": "Rows the run would cover.",
                              "example": 2
                            },
                            "columns": {
                              "type": "integer",
                              "description": "Columns that would run.",
                              "example": 2
                            },
                            "cellsToRun": {
                              "type": "integer",
                              "description": "Cells the run would execute \u2014 the unit spending actually happens in.",
                              "example": 3
                            },
                            "maxCostUsd": {
                              "type": "number",
                              "description": "Summed ceiling in US dollars over the columns that could be priced: the number a live trigger's `maxCostUsd` cap is checked against. A ceiling, not a forecast, and not the whole bill: columns counted in `unpricedColumns` are excluded from it.",
                              "example": 0.0559
                            },
                            "pricedColumns": {
                              "type": "integer",
                              "description": "Columns whose cost is included in `maxCostUsd`.",
                              "example": 2
                            },
                            "unpricedColumns": {
                              "type": "integer",
                              "description": "Columns with no price knowable before the run. They appear in `perColumn` with a null `unitUsd` and are NOT counted in `maxCostUsd`.",
                              "example": 0
                            },
                            "perColumn": {
                              "type": "array",
                              "description": "One entry per column that would run.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "columnId": {
                                    "type": "string"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "type": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "The column type: ai, api, task or reference."
                                  },
                                  "rowsToRun": {
                                    "type": "integer",
                                    "description": "Rows whose cell for this column the run would actually execute. Rows already holding a finished answer are excluded, because an unforced run does not re-run them and does not charge for them."
                                  },
                                  "unitUsd": {
                                    "type": "number",
                                    "nullable": true,
                                    "description": "Price of one row in US dollars, or null when no source could answer."
                                  },
                                  "maxUsd": {
                                    "type": "number",
                                    "nullable": true,
                                    "description": "`unitUsd` times `rowsToRun`, or null alongside a null unit price."
                                  },
                                  "basis": {
                                    "type": "string",
                                    "enum": ["ceiling", "observed_average", "pre_run_estimate", "unknown"],
                                    "description": "Which source answered. `ceiling` is a registry price the charge cannot exceed; `observed_average` is the mean of this column\u2019s own completed cells; `pre_run_estimate` is the typical-row estimate for an AI column that has never run."
                                  },
                                  "approximate": {
                                    "type": "boolean",
                                    "description": "False only for `ceiling`, which the charge cannot exceed."
                                  },
                                  "hasRunCondition": {
                                    "type": "boolean",
                                    "description": "The column carries a run condition. Conditions are NOT evaluated here, so fewer rows than `rowsToRun` may execute \u2014 never more."
                                  },
                                  "underCeiling": {
                                    "type": "array",
                                    "description": "Declared reasons the settled charge can land below `maxUsd`.",
                                    "items": {
                                      "type": "object",
                                      "properties": {
                                        "kind": {
                                          "type": "string",
                                          "enum": ["cached_repeat", "charged_on_result_only", "outcome_priced", "actual_tokens"]
                                        },
                                        "withinDays": {
                                          "type": "integer",
                                          "description": "On `cached_repeat`: the window inside which a repeat lookup of the same entity by this workspace is free."
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            },
                            "notInTable": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              },
                              "description": "Row ids you sent that belong to a different table. They were excluded from the estimate, as they would be from the run."
                            },
                            "notes": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              },
                              "description": "Everything a caller has to know to read `maxCostUsd` correctly, in prose \u2014 for a caller that reads the payload and not the docs."
                            },
                            "maxPerRowUsd": {
                              "type": "number",
                              "description": "What one row holds from the wallet before it runs: every column with cells to run, at its worst case. `wallet.rowsCovered` is counted against it. Not a column's `unitUsd`: a waterfall's `unitUsd` sums every step in the chain, while a column or waterfall step that answers in the background, such as FullEnrich or Surfe, holds its own cost when it is sent and is left out here.",
                              "example": 0.0652
                            },
                            "withinCap": {
                              "type": "boolean",
                              "description": "Present only when `maxCostUsd` was sent: whether the same call without `dryRun` would start."
                            },
                            "wallet": {
                              "type": "object",
                              "description": "What the wallet can pay for. Each row holds its worst-case cost before it runs, so a run larger than the wallet runs the rows it covers, in order, and the rest are refused rather than overdrawing it.",
                              "properties": {
                                "spendableUsd": {
                                  "type": "number",
                                  "description": "What the wallet can spend right now: the same figure as `balance.spendableUsd` from `GET /v1/wallet`.",
                                  "example": 0.06
                                },
                                "rowsCovered": {
                                  "type": "integer",
                                  "description": "How many of the priced rows that covers. Rows past it would be refused with a not-enough-balance error on their cells, and not charged.",
                                  "example": 1
                                },
                                "wouldStart": {
                                  "type": "boolean",
                                  "description": "Whether the run would start at all. False means the live call would be refused with 402 `insufficient_balance` before anything is queued.",
                                  "example": true
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "503": {
            "description": "Loopster could not hand the run to its workers, and the request to them was never sent: nothing was queued, nothing was charged, and cells this request had marked as queued were put back to what they held. Send the same request again after `Retry-After` seconds; with an `Idempotency-Key`, the same key, since this answer is not stored against it. A `500` makes no such promise: rows may have started. The body's `code` is `nothing_started`. The same answer when the workers' queue was full, with their wait in `Retry-After` (at most 120 seconds). A `500` with `code: \"nothing_started_internal\"` also means nothing was started (and for a run, its rows were removed), but the workers refused the request itself, a fault on Loopster's side: sending it again now will likely fail the same way.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "422": {
            "description": "The run was not started because of `maxCostUsd`. `code` is `cost_cap_exceeded` (the estimate is over the cap) or `cost_cap_unpriced_columns` (some columns have no up-front price and `allowUnpriced` was not sent). Nothing was queued and no spend slot was taken. An `Idempotency-Key` sent with it is released by the refusal, so the same key runs once the cap is raised or the estimate has dropped. An identical retry of a request that already completed under that key replays its first answer instead of being priced again. If the key cannot be released, the refusal is still returned and the key stays unusable until it expires; see [Idempotent Requests](/api-reference/idempotent-requests).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": ["cost_cap_exceeded", "cost_cap_unpriced_columns"]
                        },
                        "estimatedCostUsd": {
                          "type": "number",
                          "description": "The estimate the cap was checked against: the dry run's `maxCostUsd`."
                        },
                        "maxCostUsd": {
                          "type": "number",
                          "description": "The cap that was sent."
                        },
                        "unpricedColumns": {
                          "type": "integer",
                          "description": "Columns with no up-front price. Present only when there were some."
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/tables/{tableId}/webhook": {
      "get": {
        "operationId": "getTableWebhook",
        "summary": "Get webhook address",
        "tags": ["Rows"],
        "description": "Whether this table has an inbound webhook address, identified by its last four characters. Never returns the URL: the URL is the credential, so it is shown once, by Rotate Webhook Address. Reading mints nothing. Editor role required. A table whose API is switched off answers 403, as on every table route.",
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              },
              "Cache-Control": {
                "description": "Always `no-store`: the response describes a credential.",
                "schema": {
                  "type": "string",
                  "example": "no-store"
                }
              }
            },
            "description": "The webhook address, by its last four characters",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "lastFour": {
                          "type": "string",
                          "nullable": true,
                          "description": "The last four characters of the address. `null` when the table has no address yet.",
                          "example": "wxyz"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/tables/{tableId}/webhook/rotate": {
      "post": {
        "operationId": "rotateTableWebhook",
        "summary": "Rotate webhook address",
        "tags": ["Rows"],
        "description": "Mint this table's inbound webhook address, or replace the one it has. The previous address stops working immediately: anything still posting to it is refused from that moment. The new URL is in this response only; afterwards Get Webhook Address shows its last four characters, so keep it from here. Every call makes a new address. Editor role required.",
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              },
              "Cache-Control": {
                "description": "Always `no-store`: the response describes a credential.",
                "schema": {
                  "type": "string",
                  "example": "no-store"
                }
              }
            },
            "description": "The new webhook address",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "description": "The new address. Shown only here.",
                          "example": "https://app.loopster.ai/api/v1/webhooks/loopster_whk_xxxxxxxx"
                        },
                        "lastFour": {
                          "type": "string",
                          "example": "wxyz"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/tables/{tableId}/stop": {
      "post": {
        "operationId": "stopProcessing",
        "summary": "Stop processing",
        "description": "Stop all ongoing processing for a table.",
        "tags": ["Processing"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Processing stopped successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "message": {
                          "type": "string",
                          "example": "Processing stopped"
                        },
                        "cellsReleased": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Cells taken out of a running state and returned to idle. `null` when the worker did not report a figure, which is not the same as zero. Work already charged is not refunded.",
                          "example": 17
                        },
                        "rowsReleased": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Rows whose whole run was cancelled, rather than individual cells. `null` when unreported.",
                          "example": 2
                        },
                        "queuedJobsRemoved": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Queued work removed before it could start, so nothing was charged for it. `null` when unreported.",
                          "example": 8
                        },
                        "stillRunning": {
                          "type": "integer",
                          "nullable": true,
                          "description": "**Cells** \u2014 not jobs \u2014 that work already past the brake may still execute, and therefore still bill for. A job that was mid-execution is not pulled back: it walks its dependency chain and runs each column in turn, so counting jobs understated the exposure by the depth of that chain. Deliberately an upper bound rather than a forecast: a `force` run re-executes cells that already hold a value, so netting off finished cells would undercount precisely where the exposure is largest. `null` when unreported.",
                          "example": 24
                        },
                        "stillRunningRows": {
                          "type": "integer",
                          "nullable": true,
                          "description": "The row jobs behind `stillRunning` \u2014 how many runs are past the brake, as opposed to how many cells they may yet execute. Answers \"how much is loose\"; `stillRunning` answers \"how much may this cost\". `null` when unreported.",
                          "example": 8
                        },
                        "cleanupContinues": {
                          "type": "boolean",
                          "description": "`true` when the stop answered as soon as the run was cancelled and its queued work stopped being handed out, while releasing cells and removing queued jobs carries on after the response. `cellsReleased`, `rowsReleased` and `queuedJobsRemoved` then count what was done so far, so they are lower bounds; `stillRunning` and `stillRunningRows` stay upper bounds when they are numbers, and are `null` when the stop answered before it could count them: treat `null` as unknown, not as zero, since work past the brake may still be billing. Nothing new starts either way. `false` when the cleanup finished before the response.",
                          "example": false
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/runs": {
      "get": {
        "operationId": "listRuns",
        "summary": "List runs",
        "description": "This workspace's runs, newest first. Filter by `kind`, `status` or `since`. Each run reads what is stored, with no cell reads: use `GET /v1/runs/{runId}` for live counts. A function run is listed even when its function is not enabled for agent access; reading it answers `403`. A workspace keeps its 500 most recent runs, for up to 30 days.",
        "tags": ["Processing"],
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["trigger", "function", "capability"]
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["running", "complete", "completed_with_errors", "stopped"]
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Only runs started at or after this time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The nextCursor from the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "A page of runs",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "runs": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "runId": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "kind": {
                                "type": "string",
                                "enum": ["trigger", "function", "capability"]
                              },
                              "tableId": {
                                "type": "string",
                                "format": "uuid",
                                "description": "Trigger runs only."
                              },
                              "functionId": {
                                "type": "string",
                                "format": "uuid",
                                "description": "Function runs only."
                              },
                              "templateId": {
                                "type": "string",
                                "description": "Capability runs only."
                              },
                              "status": {
                                "type": "string",
                                "enum": ["running", "complete", "completed_with_errors", "stopped"],
                                "description": "What is stored. A run not settled yet reads `running`, including one that finished and has not been read since; `GET /v1/runs/{runId}` gives live counts and settles it."
                              },
                              "rows": {
                                "type": "integer"
                              },
                              "createdAt": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "settledAt": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        },
                        "nextCursor": {
                          "type": "string",
                          "nullable": true,
                          "description": "Pass as cursor for the next page; null on the last."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/runs/{runId}/cancel": {
      "post": {
        "operationId": "cancelRun",
        "summary": "Cancel a run",
        "description": "Remove the work of one run that is still queued: its rows, across the columns it ran. A row another run had already queued when this one started is not part of this run, so only that run's cancel stops it. If the queue can't be reached the answer is 500 and nothing is marked, so a retry is safe. Any editor in the run's workspace may cancel any run there. Work already running finishes and is billed: a cancel is not a refund, and the answer says how many cells were stopped and how many were already running. Cancelling a run that already finished, or cancelling twice, changes nothing and says so (`canceled: false`, with the stored status).",
        "tags": ["Processing"],
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "description": "The runId a trigger, or a batch function run, returned.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "The run after the cancel",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "runId": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "canceled": {
                          "type": "boolean",
                          "description": "True when this call cancelled the run; false when it had already finished or been cancelled."
                        },
                        "status": {
                          "type": "string",
                          "enum": ["running", "complete", "completed_with_errors", "stopped"]
                        },
                        "stoppedCells": {
                          "type": "integer",
                          "description": "Queued cells removed before they started. Nothing is charged for them."
                        },
                        "alreadyRunningCells": {
                          "type": "integer",
                          "description": "Cells already running when the cancel arrived. They finish, and are billed."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Run not found. The same answer for a run of another workspace, an id that names nothing, a run removed by the workspace's 500-run cap or its 30-day limit, a trigger run whose table is in Trash, and a function run whose function is gone.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/runs/{runId}": {
      "get": {
        "operationId": "getRun",
        "summary": "Get a run's status",
        "description": "The status of one run and only that run: the rows a trigger dispatched, and the columns it ran. `status` is `running` until every one of its cells has settled, then `complete` or `completed_with_errors`; `stopped` if a stop reached any of its cells (its counts are the cells as the stop left them). A run is never reported finished in its first 30 seconds (so a run that finishes sooner still reads `running` until then), and a run that can never finish (a dependency that never fires, a row deleted mid-run) stays `running` until the 500-run cap or the 30-day limit removes it. The first finished answer is kept: a later trigger that reruns the same cells does not make a finished run report running again. The answer is cached for up to 5 seconds. When another request is still computing it and there is no earlier answer, `cells` is null and a `Retry-After` header says when to ask again; when there is an earlier answer, it is returned with `stale: true`. A workspace keeps its 500 most recent runs (settled ones are removed first), for up to 30 days; at 60 triggers a minute that is about 8 minutes of history, so poll the runs you care about. A function run started with a list of inputs (`POST /v1/functions/{functionId}/runs`) answers here too, naming its `functionId`; so does a single function run, by the runId it returned. A capability run (`POST /v1/capabilities/{templateId}/runs`) answers here too, naming its `templateId`.",
        "tags": ["Processing"],
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "description": "The runId a trigger, a capability run or a batch function run returned.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "The run",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "runId": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "kind": {
                          "type": "string",
                          "enum": ["trigger", "function", "capability"]
                        },
                        "tableId": {
                          "type": "string",
                          "format": "uuid",
                          "description": "The table a trigger ran on. Present for trigger runs only."
                        },
                        "functionId": {
                          "type": "string",
                          "format": "uuid",
                          "description": "The function a function run ran (LOOPSTR-2459). Present for function runs only."
                        },
                        "templateId": {
                          "type": "string",
                          "description": "The capability a capability run ran (LOOPSTR-2458). Present for capability runs only; never the id of the hidden table it ran in."
                        },
                        "status": {
                          "type": "string",
                          "enum": ["running", "complete", "completed_with_errors", "stopped"]
                        },
                        "rows": {
                          "type": "integer",
                          "description": "Rows the run dispatched."
                        },
                        "cells": {
                          "type": "object",
                          "nullable": true,
                          "description": "This run's cells: its rows \u00d7 the columns whose cells decide it. Null only while another request is still computing an answer nobody has seen yet (then `Retry-After` is set).",
                          "properties": {
                            "total": {
                              "type": "integer"
                            },
                            "inFlight": {
                              "type": "integer",
                              "description": "Queued, generating, retrying or waiting on a callback."
                            },
                            "complete": {
                              "type": "integer"
                            },
                            "error": {
                              "type": "integer"
                            },
                            "skipped": {
                              "type": "integer"
                            },
                            "unsatisfied": {
                              "type": "integer"
                            },
                            "notRun": {
                              "type": "integer",
                              "description": "Cells with no status: never reached, or reset by a stop."
                            }
                          }
                        },
                        "createdAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "stale": {
                          "type": "boolean",
                          "description": "Present when this is the last answer on record because a fresh one was still being computed."
                        },
                        "settledAt": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the run finished: when the last row it queued reported back, or, for a run that settled otherwise, when Loopster first found it finished. Present once it has."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "description": "A function run whose function is not enabled for agent access. The same answer its own poll URL gives; a workspace editor can enable it in the function's settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Run not found. The same answer for a run of another workspace, an id that names nothing, a run removed by the workspace's 500-run cap or its 30-day limit, a trigger run whose table is in Trash, and a function run whose function is gone, and a capability run whose hidden table was dropped at the 50-table limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/runs/{runId}/items": {
      "get": {
        "operationId": "getRunItems",
        "summary": "List a run's rows",
        "description": "A page of a run's rows and each one's current state: `running`, `complete` (every one of its cells settled without an error; a skipped or unsatisfied cell counts as settled), `error`, `incomplete` (some of its cells settled and the rest never started, which a stop leaves behind) or `not_run`. Row ids and states, never cell values. A function run's row also names the outputs whose step was skipped, in `skipped`, with the reason. States reflect the cells now, so a later trigger that reruns a row shows it running again here even after the run itself finished. Answers `503` with `Retry-After` when another request is still reading the same page and there is no earlier answer.",
        "tags": ["Processing"],
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "description": "The runId a trigger, a capability run or a batch function run returned.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The nextCursor from the previous page. Opaque, and only valid for this run.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page, 1 to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "A page of the run's rows",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "runId": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "items": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "rowId": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "index": {
                                "type": "integer",
                                "minimum": 0,
                                "description": "For a function or capability run, the index of the input this row ran. Absent for a trigger run."
                              },
                              "status": {
                                "type": "string",
                                "enum": ["running", "complete", "error", "incomplete", "not_run", "gone"],
                                "description": "`gone`: the row was removed by its hidden table's retention (a function or capability table keeps its newest rows only), so its outcome is no longer held."
                              },
                              "output": {
                                "type": "string",
                                "description": "A capability run's result for this input, once `complete`: the value, as text."
                              },
                              "error": {
                                "type": "string",
                                "description": "Why a capability run's input failed, once `error`: the cell's message, with machine output replaced by a standard sentence, cut at 300 characters."
                              },
                              "skipped": {
                                "type": "array",
                                "description": "For a function run: the outputs this row has no value for because their step was skipped, each with its output `key` and the step's `reason`, cut at 300 characters. Present only when there is at least one, and only for a function that is open to agents. A skipped step settles the row, so its `status` is still `complete`. Absent on a trigger or capability run's rows.",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "key": {
                                      "type": "string",
                                      "description": "The output key."
                                    },
                                    "reason": {
                                      "type": "string",
                                      "description": "Why the step did not run."
                                    }
                                  }
                                }
                              }
                            }
                          }
                        },
                        "nextCursor": {
                          "type": "string",
                          "nullable": true,
                          "description": "Pass as cursor for the next page; null on the last."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "503": {
            "description": "Another request is still reading this page and there is no earlier answer; retry after `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before asking again.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "description": "A function run whose function is not enabled for agent access. The same answer its own poll URL gives; a workspace editor can enable it in the function's settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Run not found. The same answer for a run of another workspace, an id that names nothing, a run removed by the workspace's 500-run cap or its 30-day limit, a trigger run whose table is in Trash, and a function run whose function is gone, and a capability run whose hidden table was dropped at the 50-table limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/tables/{tableId}/status": {
      "get": {
        "operationId": "getStatus",
        "summary": "Get processing status",
        "description": "Get row-level and per-column processing status for a table. Both blocks are derived from cell state, so they agree with each other. The columns block lists every column the table has, whether or not it has ever run, and its counts add up to rows.total; a column whose values live on sub-columns reports under its own id, and a manual column is counted by whether its cells hold a value.",
        "tags": ["Processing"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Status retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "rows": {
                          "type": "object",
                          "description": "Row counts by status, derived from each row's cells rather than from the row's own status column.",
                          "properties": {
                            "total": {
                              "type": "integer"
                            },
                            "idle": {
                              "type": "integer"
                            },
                            "queued": {
                              "type": "integer"
                            },
                            "generating": {
                              "type": "integer"
                            },
                            "complete": {
                              "type": "integer"
                            },
                            "error": {
                              "type": "integer"
                            }
                          }
                        },
                        "columns": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "object",
                            "properties": {
                              "header": {
                                "type": "string"
                              },
                              "type": {
                                "type": "string"
                              },
                              "idle": {
                                "type": "integer"
                              },
                              "queueing": {
                                "type": "integer"
                              },
                              "queued": {
                                "type": "integer"
                              },
                              "generating": {
                                "type": "integer"
                              },
                              "complete": {
                                "type": "integer"
                              },
                              "error": {
                                "type": "integer"
                              },
                              "skipped": {
                                "type": "integer"
                              }
                            }
                          },
                          "description": "Every column of the table, keyed by column id. Counts add up to rows.total."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tables/{tableId}/views": {
      "get": {
        "operationId": "listViews",
        "summary": "List views",
        "description": "List all views for a table.",
        "tags": ["Views"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Views retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "views": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/View"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "createView",
        "summary": "Create a view",
        "description": "Create a new view with optional filters, sorting, and column visibility settings. Each filter's `id` and each sort's `id` must be a column of the table (a column inside a group included) or `__rowCreatedAt__` / `__rowUpdatedAt__`, and each filter's `operator` one of the grid's operators; otherwise the request is refused with 400 naming the problem, and nothing is stored. `columnVisibility` must map ids to true or false.",
        "tags": ["Views"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "emoji": {
                    "type": "string"
                  },
                  "filters": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "sorting": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "desc": {
                          "type": "boolean"
                        }
                      },
                      "required": ["id", "desc"]
                    }
                  },
                  "columnVisibility": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "View created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "view": {
                          "$ref": "#/components/schemas/View"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tables/{tableId}/views/{viewId}": {
      "patch": {
        "operationId": "updateView",
        "summary": "Update a view",
        "description": "Update a view's name, emoji, order, filters, sorting, or column visibility. Each filter's `id` and each sort's `id` must be a column of the table (a column inside a group included) or `__rowCreatedAt__` / `__rowUpdatedAt__`, and each filter's `operator` one of the grid's operators; otherwise the request is refused with 400 naming the problem, and nothing is stored. `columnVisibility` must map ids to true or false. Only the fields sent are judged: a stored filter on a column deleted since does not stop a rename.",
        "tags": ["Views"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "$ref": "#/components/parameters/viewId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "emoji": {
                    "type": "string"
                  },
                  "order": {
                    "type": "number"
                  },
                  "filters": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "sorting": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "desc": {
                          "type": "boolean"
                        }
                      },
                      "required": ["id", "desc"]
                    }
                  },
                  "columnVisibility": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "View updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "view": {
                          "$ref": "#/components/schemas/View"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "operationId": "deleteView",
        "summary": "Delete a view",
        "description": "Delete a view from a table.",
        "tags": ["Views"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "$ref": "#/components/parameters/viewId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "View deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "deletedViewId": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tables/{tableId}/shares": {
      "get": {
        "operationId": "listShareLinks",
        "summary": "List share links",
        "description": "The table's live share links, newest first, whoever made them. Revoked and expired links are not listed. Needs an editor key.",
        "tags": ["Share links"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "The table's live share links",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "shares": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ShareLink"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "createShareLink",
        "summary": "Create a share link",
        "description": "Create a public, read-only link to the whole table. The link belongs to the workspace, so it stays revocable after whoever set up the key has left. Needs an editor key. A function's steps cannot be shared. A link shows each column's settings without the webhook addresses, headers and tokens saved in them. Text typed into a request body travels with the link, so put credentials in a header or a token field, not in the body.",
        "tags": ["Share links"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expiresInSeconds": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 31536000,
                    "description": "How long the link works, in seconds, up to a year. Omit for a link that works until revoked."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Share link created",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "share": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/ShareLink"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "shareToken": {
                                  "type": "string",
                                  "description": "The token at the end of `url`."
                                }
                              }
                            }
                          ]
                        },
                        "ignoredKeys": {
                          "$ref": "#/components/schemas/IgnoredKeys"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tables/{tableId}/shares/{shareId}": {
      "delete": {
        "operationId": "revokeShareLink",
        "summary": "Revoke a share link",
        "description": "Revoke one share link to this table, whoever made it. The URL stops working at once and cannot be turned back on. A link that is not live on this table answers 404. Needs an editor key.",
        "tags": ["Share links"],
        "parameters": [
          {
            "$ref": "#/components/parameters/tableId"
          },
          {
            "$ref": "#/components/parameters/shareId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Share link revoked",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "revoked": {
                          "type": "boolean"
                        },
                        "shareId": {
                          "type": "string",
                          "format": "uuid"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/search": {
      "post": {
        "operationId": "searchRecords",
        "summary": "Search people or companies",
        "description": "Find people or companies in Loopster Data by criteria filters. Free to call. Up to 50 records per page; pass the response's nextCursor back as cursor for the next page. Records come back in the index's own order (ascending by record id), not by relevance. Records carry no email addresses or phone numbers; add a Loopster Data email or phone finder column to an imported table for those. A value outside a multiSelect filter's published options is a 400 naming the value; an unknown filter id is dropped and reported in warnings. 422 means Loopster Data could not answer the search inside its query time limit \u2014 the filters match too much of the index to rank, or the matches are too sparse for an unindexed scan to reach. Neither means the query is invalid. Each filter is bounded: at most 1,000 values in a textList filter's include or exclude, at most 500 in a multiSelect filter's, and at most 500 characters a value, and a numberRange's min and max at most its own ceiling (monthsInRole 1,200, employeeCount 10,000,000, foundedYear and companyFoundedYear 9,999, fundingRounds and companyFundingRounds 1,000, totalFunding and companyTotalFunding 10,000,000,000,000 USD); past any of them is a 400 naming the field. A filter GET /v1/search/filters reports as available false (hasEmail, hasPhone, pastCompany and the founding and funding ranges, until their data is published) is a 400 naming it, never run without it. A filter whose only values come from a Loopster table (`sources`) is a 400, because the API cannot read a table into a filter and searching without it would match everyone. 403 means a table named in `exclusions` has API access turned off. 429 is also returned, with Retry-After, when Loopster Data itself is rate limiting. 503 means search is not configured, temporarily unavailable, or switched off for this workspace. Every answer carries a searchId naming the query it ran: send {searchId, cursor?, limit?} in place of the query to page it again without resending the filters. A searchId lasts 24 hours and answers only in the workspace that ran the search (404 otherwise, the same as an expired one); a query over 16 KB gets none (searchId null, with a warning). The query is run again, so a later page reads the live index. A workspace may have 4 searches in flight at once, across its API keys and the app: a fifth is not run and answers 429 with Retry-After: 2; send it again once another has answered.",
        "tags": ["Search"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Either a query (entity, filters, …) or a searchId from a previous answer with at most cursor, limit and entity. entity is required with a query.",
                "properties": {
                  "searchId": {
                    "type": "string",
                    "description": "A previous answer's searchId, in place of the query. Only cursor, limit and entity may go with it; entity, if sent, must match the search's. Unknown, expired or another workspace's: 404."
                  },
                  "entity": {
                    "type": "string",
                    "enum": ["people", "companies"]
                  },
                  "filters": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Filter values keyed by filter id. GET /v1/search/filters lists the vocabulary and value shapes. Bounded per filter: a textList's include and exclude each hold at most 1,000 values, a multiSelect's at most 500, and every value is at most 500 characters; a numberRange's min and max are at most its ceiling (monthsInRole 1,200, employeeCount 10,000,000, founded year 9,999, funding rounds 1,000, total funding 10,000,000,000,000 USD). A request past any bound is a 400. People also take hasEmail and hasPhone (boolean), pastCompany (textList of company names, domains or LinkedIn company URLs, matched on past employers) and companyFoundedYear, companyTotalFunding, companyFundingRounds (numberRange); companies take foundedYear, totalFunding and fundingRounds (numberRange). A record with no founding year or funding never matches those ranges."
                  },
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 50,
                    "default": 25,
                    "description": "Records per page. A ceiling, not a guarantee: the database time limit applies per page and every page after the first costs more to reach, so a search that returns 50 on page one can be refused 50 on page two and still answer a smaller number. On a 422, retry the same cursor with a smaller limit before changing filters. Depth does not make it worse, but the end of the set is also a 422 rather than an empty page \u2014 a refusal that persists at a smaller limit means everything has been read."
                  },
                  "cursor": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 1024,
                    "description": "Opaque cursor from a previous response's nextCursor. Omit for the first page. It works only with the search that returned it: the same workspace, entity, filters, exclusions and per-company limit, sent again or named by searchId. Anything else is refused with 400 and `code: invalid_cursor`."
                  },
                  "exclusions": {
                    "type": "array",
                    "maxItems": 3,
                    "description": "Exclusion lists applied to the page, at most three. Each is either a stored list \u2014 {kind:'set', id, label, count, matches} \u2014 or a live reference to one of your tables: {kind:'list', projectId, columnId, matches}. `matches` is 'identity' to exclude the records themselves, or 'company' (people only) to exclude everyone employed by the listed companies. A list that no longer exists is a 404, never a search that quietly excludes nothing. A table list holding more than 100,000 rows is refused with 400 (the message gives its size) rather than applied in part. Reading a table list counts against a per-workspace limit of 20 a minute (429 above it). The list is then reused for up to two minutes, so the pages of a crawl read it once, and an edit to that table can take up to two minutes to affect searches; imports always read it as it stands.",
                    "items": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  },
                  "perCompanyLimit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "description": "People only: at most this many people from any one company on this page, most senior first. Applied to the page and not across a crawl \u2014 reading further pages can return up to this many more per company each time, and a warning says so when you page with it set. People whose employer is not on record are exempt individually."
                  },
                  "version": {
                    "type": "integer",
                    "enum": [1],
                    "description": "The query shape you are sending. Omit for the current one; a version this API does not know is refused rather than guessed. Echoed back as queryVersion."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Matches",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "entity": {
                          "type": "string",
                          "enum": ["people", "companies"]
                        },
                        "records": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "fields": {
                                "type": "object",
                                "additionalProperties": true
                              }
                            }
                          }
                        },
                        "approximateTotal": {
                          "type": "integer",
                          "description": "How many records match the query, before exclusions and the per-company limit. Counted for you on a first page. When totalIsLowerBound is true it is a floor: at least this many match. A later page is not counted again; take the total from the first page."
                        },
                        "totalIsLowerBound": {
                          "type": "boolean",
                          "description": "True when approximateTotal is a lower bound and not the total: show it as \"N+\". That is the case when the count ran past its ceiling, when a first page could not be counted in time, and on every later page that carries no total of its own, where the number is that page's length."
                        },
                        "firstMatchesOnly": {
                          "type": "boolean",
                          "description": "True when the page is the first matches the index found, in no particular order, and nextCursor is null. The records are real; only the order is not."
                        },
                        "isSample": {
                          "type": "boolean",
                          "description": "True only in development sample mode; never in production."
                        },
                        "searchId": {
                          "type": "string",
                          "nullable": true,
                          "description": "This query, by name: send {searchId, cursor} to page it again without the filters. The same search answers the same searchId. Lasts about 24 hours (at least 23: running the search again within the hour does not extend it), in this workspace only. A workspace keeps its 1,000 most recent searches; an older searchId answers 404 as an expired one does. Null when it could not be kept (a query over 16 KB, or the store unavailable); a warning with code search_id_unavailable says which."
                        },
                        "nextCursor": {
                          "type": "string",
                          "nullable": true,
                          "description": "Pass back as cursor for the next page. Null at the end of the result set."
                        },
                        "resultOrder": {
                          "type": "string",
                          "description": "How the records are ordered, said out loud so the first page is not mistaken for a sample."
                        },
                        "warnings": {
                          "type": "array",
                          "description": "Things that changed the query without failing it \u2014 a dropped filter id, or an exclusion value the index does not hold.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "code": {
                                "type": "string",
                                "enum": [
                                  "unknown_filter",
                                  "wrong_kind",
                                  "unknown_value",
                                  "per_company_limit_is_per_page",
                                  "search_id_unavailable"
                                ]
                              },
                              "filter": {
                                "type": "string"
                              },
                              "message": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "queryVersion": {
                          "type": "integer",
                          "description": "The version of the query shape this response was produced for."
                        },
                        "applied": {
                          "type": "object",
                          "nullable": true,
                          "description": "What the page-level rules removed, present only when they were asked for \u2014 so a thin page can be told from a thin match set. approximateTotal is counted before both.",
                          "properties": {
                            "exclusions": {
                              "type": "object",
                              "properties": {
                                "identities": {
                                  "type": "integer"
                                },
                                "removedFromPage": {
                                  "type": "integer"
                                }
                              }
                            },
                            "perCompanyLimit": {
                              "type": "object",
                              "properties": {
                                "limit": {
                                  "type": "integer"
                                },
                                "removedFromPage": {
                                  "type": "integer"
                                },
                                "companies": {
                                  "type": "integer"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "400": {
            "description": "Invalid or empty query, a filter value outside the published vocabulary, a filter past its bounds (1,000 typed values, 500 picked options, 500 characters a value, a range past its ceiling), a filter whose data is not published yet (available false in GET /v1/search/filters), or a filter whose only values come from a table (`sources`). A filter whose data is not published yet carries `code: filter_not_available` and `filters`, the id of every such filter the query named; drop them, or wait until GET /v1/search/filters lists them as available. Also a `cursor` this search did not return: one that is made up or cut short, or one from a search with a different entity, filters, exclusions or per-company limit. That refusal carries `code: invalid_cursor`, and its remedy is to start again, not to change the query: leave `cursor` out to start from the first page",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/ErrorResponse" },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": ["filter_not_available", "invalid_cursor"],
                          "description": "Present only on these two refusals. Every other 400 here carries no code."
                        },
                        "filters": {
                          "type": "array",
                          "items": { "type": "string" },
                          "description": "With `filter_not_available`: the id of every filter the query named whose data is not published yet.",
                          "example": ["hasEmail", "hasPhone"]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No search with this searchId in this workspace (another workspace's, expired after 24 hours, or never issued), or a list named in `exclusions` no longer exists",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "A table named in `exclusions` has API access turned off, so an API key cannot read it. Turn API access on for that table, or exclude by a list of values instead",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Loopster Data could not answer the search inside its query time limit"
          },
          "503": {
            "description": "Search not configured or temporarily unavailable, or switched off for this workspace (paused, or not yet released to this workspace). A switched-off search answers \"Searching the Loopster database is temporarily switched off\" and does not clear on an immediate retry. Nothing you have imported is affected"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/search/filters": {
      "get": {
        "operationId": "listSearchFilters",
        "summary": "List search filters",
        "description": "The filter vocabulary for POST /v1/search: filter ids, kinds, labels and one options list per filter, labelled with where it came from. Read this before building a search query. Also returns the value envelope each kind accepts and a glossary of every field the search endpoints return. 503 means search is switched off for this workspace (paused, or not yet released to this workspace) \u2014 the same condition in which POST /v1/search answers 503. It is not a transient fault, so retrying at once will not help.",
        "tags": ["Search"],
        "parameters": [
          {
            "name": "entity",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": ["people", "companies"]
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Filter vocabulary",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "entity": {
                          "type": "string"
                        },
                        "filters": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              },
                              "kind": {
                                "type": "string"
                              },
                              "help": {
                                "type": "string",
                                "nullable": true
                              },
                              "excludable": {
                                "type": "boolean"
                              },
                              "options": {
                                "type": "array",
                                "nullable": true,
                                "items": {
                                  "type": "object"
                                }
                              },
                              "optionsSource": {
                                "type": "string",
                                "nullable": true,
                                "enum": ["live", "fallback", null],
                                "description": "'live': published by Loopster Data and enforced on search. 'fallback': a hand-maintained list served because the index could not be reached, and enforced on nothing. Null: this filter has no vocabulary."
                              },
                              "indexed": {
                                "type": "boolean",
                                "nullable": true,
                                "description": "True when the database has an index on this filter. False means a search touching it walks rows and can exhaust the query time limit \u2014 not that it is unusable. Null means the source did not say."
                              },
                              "available": {
                                "type": "boolean",
                                "nullable": true,
                                "description": "Whether the deployed source accepts this filter. Null means it did not say. A filter searchable only once its data is published (hasEmail, hasPhone, pastCompany, the founding and funding ranges) is false until the source lists it, and a search naming it is refused with 400 until then."
                              },
                              "textMatchModes": {
                                "type": "array",
                                "nullable": true,
                                "items": {
                                  "type": "string",
                                  "enum": ["contains", "exact"]
                                }
                              }
                            }
                          }
                        },
                        "vocabulary": {
                          "type": "object",
                          "description": "Where filters[].options came from, when it was fetched, and whether it is stale.",
                          "properties": {
                            "source": {
                              "type": "string",
                              "enum": ["live", "fallback"]
                            },
                            "stale": {
                              "type": "boolean"
                            },
                            "fetchedAt": {
                              "type": "string",
                              "nullable": true,
                              "format": "date-time"
                            },
                            "notice": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "valueEnvelopes": {
                          "type": "array",
                          "description": "The value shape each kind accepts, with an example that validates.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "kind": {
                                "type": "string"
                              },
                              "shape": {
                                "type": "string"
                              },
                              "example": {
                                "type": "object",
                                "additionalProperties": true
                              },
                              "notes": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "glossary": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string"
                          },
                          "description": "Every field the search endpoints return, explained."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "400": {
            "description": "Missing or unknown entity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Search is switched off for this workspace (paused, or not yet released to this workspace). It is not a transient fault, so retrying at once will not help. Nothing you have imported is affected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/feedback": {
      "post": {
        "operationId": "submitFeedback",
        "summary": "Submit feedback",
        "description": "Submit feedback from API consumers or LLM agents. The message and context reach the team as text (markup is escaped). Each workspace can send 30 an hour; past that it answers 429 and nothing is sent. MCP: send_feedback.",
        "tags": ["Feedback"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["message"],
                "properties": {
                  "message": {
                    "type": "string",
                    "description": "Feedback message."
                  },
                  "type": {
                    "type": "string",
                    "enum": ["missing_integration", "docs_gap", "user_complaint", "implementation_failure", "general"],
                    "default": "general",
                    "description": "Feedback type."
                  },
                  "context": {
                    "type": "object",
                    "description": "Additional context."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Feedback received",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "received": {
                          "type": "boolean"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/connections": {
      "get": {
        "operationId": "listConnections",
        "summary": "List connections",
        "description": "List the accounts this workspace can run task columns on: its own connections, then any a sibling workspace shared into the organization. An id from here is what integrationConnectionId takes on a column. Only status \"connected\" can run. Connections are added in Loopster under Settings > Connections; this API cannot add one, but can test one (Test a connection) and remove one no table uses (Remove a connection). Loopster's own account is not a connection and is not listed: a task that offers it runs on it when its taskConfig sets execution_mode to \"managed\".",
        "tags": ["Account"],
        "parameters": [
          {
            "name": "provider",
            "in": "query",
            "required": false,
            "description": "Only this provider's connections, e.g. apollo or hubspot. An unknown provider is rejected with 400 naming the valid ones.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Connections retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessResponse" },
                    {
                      "type": "object",
                      "properties": {
                        "connections": { "type": "array", "items": { "$ref": "#/components/schemas/Connection" } },
                        "note": {
                          "type": "string",
                          "description": "Where connections are added, and how to run on Loopster's own account instead. Present on every response."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          }
        }
      }
    },
    "/v1/connections/{connectionId}": {
      "delete": {
        "operationId": "removeConnection",
        "summary": "Remove a connection",
        "description": "Remove one of this workspace's own connections. Refused with 409 when any table still runs columns on it (connection_in_use, naming the tables) or when it is shared with the organization (connection_shared), and with 400 for an OAuth connection (remove_in_app), which the app removes because it also revokes the access at the provider. Needs an editor key with a recorded creator (403 key_has_no_creator otherwise). Each removal is audited against the key. Limited to 10 connection changes an hour and 50 a day per key, and 30 an hour and 100 a day per workspace (429 connection_write_limited). Idempotency-Key is refused with 400; a repeated removal answers 404.",
        "tags": ["Account"],
        "parameters": [
          {
            "name": "connectionId",
            "in": "path",
            "required": true,
            "description": "The connection id, from List Connections.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Connection removed",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "removed": {
                          "type": "boolean",
                          "enum": [true]
                        },
                        "connection": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "provider": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Refused: in use, or shared with the organization. Nothing was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [false]
                    },
                    "code": {
                      "type": "string",
                      "enum": ["connection_in_use", "connection_shared"]
                    },
                    "error": {
                      "type": "string"
                    },
                    "tables": {
                      "type": "integer",
                      "description": "connection_in_use: how many tables run columns on it."
                    },
                    "tableIds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "connection_in_use: up to 20 of those tables' ids."
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/connections/{connectionId}/test": {
      "post": {
        "operationId": "testConnection",
        "summary": "Test a connection",
        "description": "Ask the provider whether it still accepts the connection's stored key. ok: false carries reason rejected (the provider refused it) or unverifiable (it could not answer, for a provider whose check counts that as a failure). ok: true means the key was not refused; for providers whose check treats an unreachable provider as no evidence against the key, that includes no answer. Only providers Loopster has a credential check for can be tested; the rest answer 400 not_testable, naming the ones that can. Changes nothing and never returns the key or the provider's text. Each test is a call on the workspace's provider account: 10 a day per workspace and provider (429 connection_check_limited). A connection that is not connected answers 409.",
        "tags": ["Account"],
        "parameters": [
          {
            "name": "connectionId",
            "in": "path",
            "required": true,
            "description": "The connection id, from List Connections.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "The provider's answer",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "required": ["ok"],
                      "properties": {
                        "connection": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "provider": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            }
                          }
                        },
                        "ok": {
                          "type": "boolean"
                        },
                        "reason": {
                          "type": "string",
                          "enum": ["rejected", "unverifiable"],
                          "description": "Present when ok is false."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The connection is not connected, so there is no key to test.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [false]
                    },
                    "code": {
                      "type": "string",
                      "enum": ["connection_not_connected"]
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/provider-defaults": {
      "get": {
        "operationId": "listProviderDefaults",
        "summary": "List provider defaults",
        "description": "Which account each provider runs on in this workspace when a column names none: one of the workspace's own connections, or Loopster's account. A provider that is not listed has no default. A default that names one of the workspace's own connections applies to existing columns that name no connection as well as new ones. A default of Loopster's account applies to columns created afterwards only: an existing column keeps the account it was saved with. Either takes effect within about 10 seconds. Any role.",
        "tags": ["Account"],
        "responses": {
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "The workspace's default accounts",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "note": {
                          "type": "string"
                        },
                        "defaults": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "provider": {
                                "type": "string"
                              },
                              "providerName": {
                                "type": "string"
                              },
                              "account": {
                                "type": "string",
                                "enum": ["loopster", "connection", "none"],
                                "description": "loopster: Loopster's account. connection: one of the workspace's own connections, named by connectionId. none: nothing chosen."
                              },
                              "connectionId": {
                                "type": "string",
                                "format": "uuid",
                                "nullable": true,
                                "description": "Set only when account is connection."
                              },
                              "connectionName": {
                                "type": "string",
                                "nullable": true,
                                "description": "Set only when account is connection."
                              }
                            }
                          }
                        },
                        "loopsterAccountProviders": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Every provider Loopster's account can be chosen for."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/provider-defaults/{provider}": {
      "put": {
        "operationId": "setProviderDefault",
        "summary": "Set a provider default",
        "description": "Choose the account a provider runs on by default in this workspace. Send exactly one of connectionId (one of the workspace's own connections for this provider) or account: \"loopster\". A default that names one of the workspace's own connections applies to existing columns that name no connection as well as new ones. A default of Loopster's account applies to columns created afterwards only: an existing column keeps the account it was saved with. Either takes effect within about 10 seconds. Any connection id that is not this workspace's own connection for this provider answers 404 connection_not_found. Loopster's account is refused with 400 no_loopster_account for a provider that has none, and with 403 spend_not_allowed for a key created with spending turned off. Sending the same choice again answers changed: false. Needs an editor key. Every change is recorded with the key that made it.",
        "tags": ["Account"],
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "description": "The provider id, as List Connections names it (for example icypeas).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "required": ["account"],
                    "additionalProperties": false,
                    "properties": {
                      "account": {
                        "type": "string",
                        "enum": ["loopster"]
                      }
                    }
                  },
                  {
                    "type": "object",
                    "required": ["connectionId"],
                    "additionalProperties": false,
                    "properties": {
                      "connectionId": {
                        "type": "string",
                        "format": "uuid",
                        "description": "A connection id from List Connections."
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "The default as it now stands",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "changed": {
                          "type": "boolean",
                          "description": "False when the default was already what was asked for. Nothing is recorded for a request that changes nothing."
                        },
                        "default": {
                          "type": "object",
                          "properties": {
                            "provider": {
                              "type": "string"
                            },
                            "providerName": {
                              "type": "string"
                            },
                            "account": {
                              "type": "string",
                              "enum": ["loopster", "connection", "none"],
                              "description": "loopster: Loopster's account. connection: one of the workspace's own connections, named by connectionId. none: nothing chosen."
                            },
                            "connectionId": {
                              "type": "string",
                              "format": "uuid",
                              "nullable": true,
                              "description": "Set only when account is connection."
                            },
                            "connectionName": {
                              "type": "string",
                              "nullable": true,
                              "description": "Set only when account is connection."
                            }
                          }
                        },
                        "previous": {
                          "type": "object",
                          "properties": {
                            "account": {
                              "type": "string",
                              "enum": ["loopster", "connection", "none"]
                            },
                            "connectionId": {
                              "type": "string",
                              "format": "uuid",
                              "nullable": true
                            }
                          }
                        },
                        "reach": {
                          "type": "string",
                          "description": "What the change applies to, in a sentence."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "clearProviderDefault",
        "summary": "Clear a provider default",
        "description": "Remove this workspace's default account for a provider, back to nothing chosen. Columns that name no connection go back to the workspace's oldest key for the provider, and new columns made through the API no longer start on Loopster's account for it. Takes effect within about 10 seconds. A provider with no default answers changed: false, so a retry is safe. Needs an editor key; a key created with spending turned off may do this. Every change is recorded with the key that made it.",
        "tags": ["Account"],
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "description": "The provider id, as List Connections names it (for example icypeas).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "The default was cleared, or there was none",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "changed": {
                          "type": "boolean",
                          "description": "False when the default was already what was asked for. Nothing is recorded for a request that changes nothing."
                        },
                        "default": {
                          "type": "object",
                          "properties": {
                            "provider": {
                              "type": "string"
                            },
                            "providerName": {
                              "type": "string"
                            },
                            "account": {
                              "type": "string",
                              "enum": ["loopster", "connection", "none"],
                              "description": "loopster: Loopster's account. connection: one of the workspace's own connections, named by connectionId. none: nothing chosen."
                            },
                            "connectionId": {
                              "type": "string",
                              "format": "uuid",
                              "nullable": true,
                              "description": "Set only when account is connection."
                            },
                            "connectionName": {
                              "type": "string",
                              "nullable": true,
                              "description": "Set only when account is connection."
                            }
                          }
                        },
                        "previous": {
                          "type": "object",
                          "properties": {
                            "account": {
                              "type": "string",
                              "enum": ["loopster", "connection", "none"]
                            },
                            "connectionId": {
                              "type": "string",
                              "format": "uuid",
                              "nullable": true
                            }
                          }
                        },
                        "reach": {
                          "type": "string",
                          "description": "What the change applies to, in a sentence."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/contexts": {
      "get": {
        "operationId": "listContexts",
        "summary": "List contexts",
        "description": "List the workspace's contexts: the ICPs, company descriptions and tier lists columns read by id. A score-prospect column requires one as taskConfig.scoreProspect.contextId, and a prompt can embed one as @{ctx:<id>}. Titles and kinds only; the text is not returned. Contexts are created in Loopster; this API cannot create one.",
        "tags": ["Account"],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Contexts retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessResponse" },
                    {
                      "type": "object",
                      "properties": {
                        "contexts": { "type": "array", "items": { "$ref": "#/components/schemas/Context" } },
                        "note": {
                          "type": "string",
                          "description": "That contexts are created in Loopster, and how a column uses one. Present on every response."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          }
        }
      }
    },
    "/v1/capabilities": {
      "get": {
        "operationId": "listCapabilities",
        "summary": "List capabilities",
        "description": "The enrichment actions `POST /v1/capabilities/{templateId}/runs` runs without a table (LOOPSTR-2458): each with the inputs it takes per item, the settings it takes once per run, whether it needs an account of the caller's, and its price per input. The same allow-list for every workspace.",
        "tags": ["Capabilities"],
        "responses": {
          "200": {
            "description": "The capabilities",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "capabilities": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "templateId": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              },
                              "description": {
                                "type": "string"
                              },
                              "provider": {
                                "type": "string"
                              },
                              "inputs": {
                                "type": "array",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "key": {
                                      "type": "string"
                                    },
                                    "label": {
                                      "type": "string"
                                    },
                                    "required": {
                                      "type": "boolean"
                                    }
                                  }
                                }
                              },
                              "config": {
                                "type": "array",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "key": {
                                      "type": "string"
                                    },
                                    "label": {
                                      "type": "string"
                                    },
                                    "required": {
                                      "type": "boolean"
                                    },
                                    "type": {
                                      "type": "string",
                                      "description": "select, checkbox, number, chip or dynamic-select."
                                    }
                                  }
                                }
                              },
                              "connection": {
                                "type": "string",
                                "enum": ["required", "optional", "none"],
                                "description": "`required`: a run needs an account of the workspace's with this provider. `optional`: one can be named; without one the workspace's saved default runs it (Loopster's included, billed to the wallet), else its own key, never Loopster's account unless that is the saved default or the action runs only on Loopster's account. `none`: nothing to choose."
                              },
                              "cost": {
                                "type": "object",
                                "description": "The price per input, as the column catalog shows it: `kind` is `free`, `byok`, `usage_based` or `per_row` (with `usd`).",
                                "additionalProperties": true
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/capabilities/{templateId}/runs": {
      "post": {
        "operationId": "runCapability",
        "summary": "Run a capability",
        "description": "Run one enrichment action on one input or a list of up to 1,000, without a table (LOOPSTR-2458). Each input bills exactly as the same column would in a table, through the same wallet, spending-limit and organization checks, unless it runs on the caller's own connection. Each input holds its worst-case cost before it runs and is charged what it really cost, never more; inputs past what the wallet covers are refused, not run, and nothing is charged for them. A run that cannot cover even its first input is refused with 402 `insufficient_balance` before anything is created. Inputs whose shape is right run; the rest come back by index in `rejected` with a fixed reason, never echoing a value, and a list where none can run is a 400. A list counts as one start per 100 accepted inputs against the per-minute spending limit. Results are kept only while the capability's hidden table is: its newest 1,000 inputs (older items read `gone`), and at most 50 configurations per workspace, the least recently used dropped first with its runs. A configuration used in the last hour, or with a run still going, is never dropped: the new one is refused with 409 `capability_table_limit`.",
        "tags": ["Capabilities"],
        "parameters": [
          {
            "name": "templateId",
            "in": "path",
            "required": true,
            "description": "The capability, from `GET /v1/capabilities`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["inputs"],
                "properties": {
                  "inputs": {
                    "oneOf": [
                      {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string"
                        },
                        "description": "Input values keyed by the capability's input keys."
                      },
                      {
                        "type": "array",
                        "minItems": 1,
                        "maxItems": 1000,
                        "items": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string"
                          }
                        },
                        "description": "A list of input objects, each run as its own input. At most 1,000, and the body at most 4,000,000 bytes."
                      }
                    ]
                  },
                  "config": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "The capability's settings for the whole run, keyed by the `config` keys `GET /v1/capabilities` names."
                  },
                  "integrationConnectionId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Run on this connection of the workspace's, or one shared into its organization; it must be connected. Without one, the run starts on the workspace's saved default account for the provider (Loopster's included, billed to your wallet), else on the workspace's own key; it never starts on Loopster's account unless that is the saved default or the action runs only on Loopster's account, and with neither the run is refused with 400."
                  },
                  "maxCostUsd": {
                    "type": "number",
                    "minimum": 0,
                    "description": "Refuse the run with 422, starting nothing, if the accepted inputs could cost more than this from the wallet."
                  },
                  "allowUnpriced": {
                    "type": "boolean",
                    "description": "With maxCostUsd: run even when the price is not known up front, including a search priced only as an estimate because it returns as many results as you ask for."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The run started",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              },
              "X-RateLimit-Spend-Limit": {
                "$ref": "#/components/headers/RateLimitSpendLimit"
              },
              "X-RateLimit-Spend-Remaining": {
                "$ref": "#/components/headers/RateLimitSpendRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "runId": {
                          "type": "string",
                          "format": "uuid",
                          "description": "The run's id for `GET /v1/runs/{runId}` and its `/items`, where each input's `output` or `error` is read. Absent in the rare case the run could not be recorded (then `message` says so)."
                        },
                        "status": {
                          "type": "string",
                          "enum": ["running"]
                        },
                        "accepted": {
                          "type": "integer",
                          "description": "For a list: how many inputs the queue took and are running. An input written but refused by the queue is in `notStarted`, not here."
                        },
                        "items": {
                          "type": "array",
                          "description": "For a list: each input the queue took, with its index and the row it runs as.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "index": {
                                "type": "integer",
                                "minimum": 0
                              },
                              "rowId": {
                                "type": "string",
                                "format": "uuid"
                              }
                            }
                          }
                        },
                        "rejected": {
                          "type": "array",
                          "description": "For a list: inputs that could not run, by index, at most the first 100 (see `rejectedCount`, `truncated`).",
                          "items": {
                            "type": "object",
                            "properties": {
                              "index": {
                                "type": "integer",
                                "minimum": 0
                              },
                              "reason": {
                                "type": "string",
                                "enum": ["wrong_type", "missing_field", "too_long"]
                              }
                            }
                          }
                        },
                        "rejectedCount": {
                          "type": "integer"
                        },
                        "truncated": {
                          "type": "boolean"
                        },
                        "message": {
                          "type": "string"
                        },
                        "notStarted": {
                          "type": "array",
                          "description": "For a list: inputs written but refused by the queue (for example when it is full). They will not run; send them again.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "index": {
                                "type": "integer",
                                "minimum": 0
                              },
                              "rowId": {
                                "type": "string",
                                "format": "uuid"
                              }
                            }
                          }
                        },
                        "notStartedCount": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid: an unknown setting in `config`, a connection that cannot be used, or a capability that needs a connection the workspace lacks. For a list of inputs where none can run, the body also lists every input by index with the reason (`wrong_type`: not an object, an unknown key, or a value that is not text; `missing_field`: a required input is missing or blank; `too_long`: over 10,000 characters), never echoing a value, and nothing was started.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success", "error"],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [false]
                    },
                    "error": {
                      "type": "string"
                    },
                    "rejected": {
                      "type": "array",
                      "description": "For a list: the inputs that could not run, at most the first 100.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "index": {
                            "type": "integer",
                            "minimum": 0
                          },
                          "reason": {
                            "type": "string",
                            "enum": ["wrong_type", "missing_field", "too_long"]
                          }
                        }
                      }
                    },
                    "rejectedCount": {
                      "type": "integer"
                    },
                    "truncated": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The function is not enabled for agent access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No capability by that id. `GET /v1/capabilities` lists them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "At the 50-configuration limit with none unused for the last hour: `code` is `capability_table_limit`, and nothing was created or started. Also answered for an Idempotency-Key that is still in use, was used for a different request, or whose first request never finished; `code` says which (see Idempotent Requests).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "`capability_table_limit`, or one of the Idempotency-Key conflict codes."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 4,000,000 bytes. Nothing was started; split the list into smaller calls.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "maxCostUsd would be exceeded, or the price is unknown and allowUnpriced was not sent. Nothing was started.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Nothing was created or started, and the same request can be sent again after the `Retry-After` seconds; with an `Idempotency-Key`, the same key, since this answer is not stored against it. Two causes. With `code: \"accounts_unavailable\"`: the workspace's connections and saved default accounts could not be read, and they decide which account this would run on. With `code: \"nothing_started\"`: Loopster could not hand the run to its workers and the request to them was never sent, so the rows created for this run were removed. A `500` makes no such promise: the run may have been created, so retry it with a new key. The same answer when the workers' queue was full, with their wait in `Retry-After` (at most 120 seconds). A `500` with `code: \"nothing_started_internal\"` also means nothing was started (and for a run, its rows were removed), but the workers refused the request itself, a fault on Loopster's side: sending it again now will likely fail the same way.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "example": 5
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/functions": {
      "get": {
        "operationId": "listFunctions",
        "summary": "List agent-callable functions",
        "description": "List the workspace's functions that are published, active, and enabled for agent access. Each entry carries the input schema and output keys the run endpoint works with, plus a per-run cost estimate (usage-priced steps such as AI models are counted in unpricedSteps rather than in credits).",
        "tags": ["Functions"],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Functions retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "functions": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/FunctionSummary"
                          }
                        },
                        "note": {
                          "type": "string",
                          "description": "What the list leaves out. It holds the functions an agent may call: published, active, and enabled for agent access. One created with POST /v1/functions appears once it is published and opened to agents; GET /v1/functions/{functionId} shows where it stands."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "createFunction",
        "summary": "Create a function",
        "description": "Create a function. A function is always created as a draft: nothing can call it until it is published, which takes an admin key. With `fromTable`, it is built from columns that already work in a table, as Save as function does in Loopster: the columns named become its steps, the columns they read but were not named become its inputs, and its outputs are chosen, so the draft is ready to publish. The key must be able to read that table, and a column whose kind the API cannot configure as a step yet is refused by name with 400 `step_not_allowed`, creating nothing. Without `fromTable` the draft is empty: open a sandbox, add steps, declare inputs and outputs, then publish. Needs an editor key. No agent can run a new function until an admin key publishes it and opens it with PUT /v1/functions/{functionId}/agent-access. The answer never carries the id of the table a function keeps its steps in; the function id is the only handle.",
        "tags": ["Functions"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["title"],
                "additionalProperties": false,
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 256
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 2000
                  },
                  "fromTable": {
                    "type": "object",
                    "required": ["tableId", "columnIds"],
                    "additionalProperties": false,
                    "description": "Build the function from columns of a table this key can read.",
                    "properties": {
                      "tableId": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "columnIds": {
                        "type": "array",
                        "minItems": 1,
                        "maxItems": 50,
                        "items": {
                          "type": "string"
                        },
                        "description": "The columns that become the function's steps."
                      },
                      "outputColumnIds": {
                        "type": "array",
                        "maxItems": 200,
                        "items": {
                          "type": "string"
                        },
                        "description": "The table columns whose results go back to callers. Omit to return every step's result."
                      },
                      "optionalInputColumnIds": {
                        "type": "array",
                        "maxItems": 200,
                        "items": {
                          "type": "string"
                        },
                        "description": "The table columns that become inputs a run may leave out. Every other input is required. A column here that does not become an input is refused with 400."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Function created",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "childColumnsBuilt": { "$ref": "#/components/schemas/BuiltChildColumns" },
                        "function": {
                          "$ref": "#/components/schemas/FunctionDetail"
                        },
                        "warnings": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Present when columns were left out of a `fromTable` function, naming them."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid: no title, an unknown field, a `fromTable` selection that names no usable column, or one that would return nothing. With `code: step_not_allowed`, a `fromTable` column is of a kind the API cannot configure as a step yet; `refusedColumns` names them and nothing was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "description": "The key's role is below what this needs (the body carries `requiredRole` and `role`), or the function is owner-only and this key is neither its owner's nor an admin's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`fromTable.tableId` names no table this key can read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/functions/{functionId}": {
      "delete": {
        "operationId": "deleteFunction",
        "summary": "Delete a function",
        "description": "Moves the function to Trash for 30 days, as Delete in the app does; POST /v1/trash/{functionId}/restore brings it back. Every table and agent that calls it stops, so a published function needs an admin key and a draft an editor key; an owner-only function also needs its owner or an admin. Audited.",
        "tags": ["Functions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/functionId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "X-RateLimit-Scope": { "$ref": "#/components/headers/RateLimitScope" }
            },
            "description": "Moved to Trash",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "deleted": {
                          "type": "boolean",
                          "enum": [true]
                        },
                        "inTrash": {
                          "type": "boolean",
                          "enum": [true]
                        },
                        "usedBy": {
                          "type": "object",
                          "description": "How many tables have columns calling the function. They stop working now.",
                          "properties": { "tables": { "type": "integer" } }
                        },
                        "function": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "title": {
                              "type": "string"
                            }
                          }
                        },
                        "restore": {
                          "type": "string",
                          "description": "The request that brings it back from Trash."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key's role is below what this function needs (admin for a published function), or the function is owner-only and the key is neither its owner's nor an admin's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No function with this id in this workspace, or it is already in Trash.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "get": {
        "operationId": "getFunction",
        "summary": "Get a function",
        "description": "One function, whatever state it is in. GET /v1/functions lists only what an agent may call, so a draft, a function not yet opened to agents and one with a sandbox open are invisible there. `contract` is what callers run today. `draft` is the body being edited, the sandbox while one is open, and is where the ids for inputs, steps and outputs come from. `costEstimate` is what one run costs now, and `draft.costEstimate` what it would cost as drafted.",
        "tags": ["Functions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/functionId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "The function",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "function": {
                          "$ref": "#/components/schemas/FunctionDetail"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "description": "No function with this id in this workspace. A function in another workspace, one in Trash, an id that names nothing and the id of anything that is not a function all answer the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/functions/{functionId}/sandbox": {
      "post": {
        "operationId": "openFunctionSandbox",
        "summary": "Open a function's sandbox",
        "description": "Open a sandbox: a copy of the function's steps that inputs, outputs and steps are edited in, leaving what callers run untouched until it is published. Needs an editor key. Opening twice is safe: a second call while one is open answers 200 with `opened: false` and changes nothing.",
        "tags": ["Functions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/functionId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "rowCount": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "How many of the function's recent rows to copy into the sandbox to test on. Omit for the default."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "A sandbox was already open; nothing changed",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "sandbox": {
                          "type": "object",
                          "properties": {
                            "opened": {
                              "type": "boolean"
                            }
                          }
                        },
                        "function": {
                          "$ref": "#/components/schemas/FunctionDetail"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "201": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Sandbox opened",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "childColumnsBuilt": { "$ref": "#/components/schemas/BuiltChildColumns" },
                        "childColumnsNotBuilt": {
                          "type": "object",
                          "description": "Present when a waterfall step lacked columns this request could not build. The sandbox is open all the same, and a publish is refused with the same `code` until the reason clears. `waterfall_running`: rows of a waterfall that would gain its result column are running. `column_limit`: the columns would take the sandbox past the workspace's column limit. `sandbox_changed`: the sandbox kept changing while they were being built.",
                          "properties": {
                            "code": { "type": "string", "enum": ["waterfall_running", "column_limit", "sandbox_changed"] },
                            "error": { "type": "string" },
                            "runningCells": {
                              "type": "integer",
                              "description": "With `waterfall_running`: how many of the waterfall's cells are still running."
                            }
                          },
                          "required": ["code", "error"]
                        },
                        "sandbox": {
                          "type": "object",
                          "properties": {
                            "opened": {
                              "type": "boolean"
                            },
                            "seededRows": {
                              "type": "integer",
                              "description": "Rows copied in to test on."
                            }
                          }
                        },
                        "function": {
                          "$ref": "#/components/schemas/FunctionDetail"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`rowCount` is not a whole number in range, or the body carries an unknown field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "description": "The key's role is below what this needs (the body carries `requiredRole` and `role`), or the function is owner-only and this key is neither its owner's nor an admin's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No function with this id in this workspace. A function in another workspace, one in Trash, an id that names nothing and the id of anything that is not a function all answer the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/functions/{functionId}/sandbox/columns": {
      "post": {
        "operationId": "addFunctionStep",
        "summary": "Add a step to a function's sandbox",
        "description": "Add a step, or several with `{ \"columns\": [ \u2026 ] }`, to the function's open sandbox. A step is a column built from a template, and the body is exactly what `POST /v1/tables/{tableId}/columns` takes, checked by the same code: `templateId`, `header`, and any settings. Only some steps can be configured through the API today: AI columns, and the tasks `icypeasFindEmail`, `hunterFindEmail`, `waterfallEmail`, `formula`, the twelve PredictLeads actions (`predictLeadsEnrichCompany`, `predictLeadsEnrichCompanyExtended`, `predictLeadsFindConnections`, `predictLeadsFindFinancing`, `predictLeadsFindJobs`, `predictLeadsFindLookalikes`, `predictLeadsFindNews`, `predictLeadsFindTechStack`, `predictLeadsGithubRepos`, `predictLeadsProducts`, `predictLeadsSecFilings`, `predictLeadsWebsiteSubpages`), the ten AI Ark actions (`aiArkEnrichPerson`, `aiArkFindWorkEmail`, `aiArkPersonalityProfile`, `aiArkFindMobilePhone`, `aiArkReverseEmailLookup`, `aiArkEnrichCompany`, `aiArkFindLookalikeCompanies`, `aiArkFindPeopleAtCompany`, `aiArkCountPeople`, `aiArkFindRecentlyJoined`), `clearoutPhoneValidatePhone`, `reverseContactDomainToCompany`, the six Loopster Data actions (`loopsterDataFindWorkEmail`, `loopsterDataFindPersonalEmail`, `loopsterDataFindPhoneNumber`, `loopsterDataEnrichPerson`, `loopsterDataEmailToLinkedin`, `loopsterDataEnrichCompany`), the nine Aviato actions (`aviatoFindPersonalEmail`, `aviatoFindHashedEmail`, `aviatoReverseEmailLookup`, `aviatoNormalizeLocation`, `aviatoCompanyFinancials`, `aviatoCompanyWebTraffic`, `aviatoCompanyLegal`, `aviatoCompanyAcquisitions`, `aviatoCompanySocialMetrics`), and these actions, which answer in the background while the function waits: the three Wiza actions (`wizaFindWorkEmail`, `wizaFindMobilePhone`, `wizaFindProfileDetails`), the four FullEnrich actions (`fullEnrichFindWorkEmail`, `fullEnrichFindPersonalEmail`, `fullEnrichFindMobilePhone`, `fullEnrichReverseEmailLookup`), the two BetterContact actions (`betterContactFindWorkEmail`, `betterContactFindMobilePhone`), `reverseContactFindWorkEmail`, and the six Surfe actions (`surfeFindWorkEmail`, `surfeFindPersonalEmail`, `surfeFindMobilePhone`, `surfeFindProfile`, `surfeReverseEmailLookup`, `surfeEnrichCompany`). Anything else is refused by name and nothing is stored. To make an AI step read an input, mention the input's id in the prompt as `@{inputId}`. The function is named by its id; no table or body id is accepted, and a function's live body is never written: without an open sandbox this answers 409. Nobody's runs see the step until the sandbox is published. Needs an editor key. Each step in the answer carries `costEstimate`: what one run of it costs.",
        "tags": ["Functions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/functionId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  { "$ref": "#/components/schemas/CreateColumnRequest" },
                  {
                    "type": "object",
                    "required": ["columns"],
                    "additionalProperties": false,
                    "properties": {
                      "columns": {
                        "type": "array",
                        "minItems": 1,
                        "maxItems": 20,
                        "description": "The columns to create, in order, each a single create's body. A batch takes no other top-level key.",
                        "items": { "$ref": "#/components/schemas/CreateColumnRequest" }
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Column created, or for a batch, every column created",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "childColumnsBuilt": { "$ref": "#/components/schemas/BuiltChildColumns" },
                        "column": {
                          "$ref": "#/components/schemas/FunctionStepColumn"
                        },
                        "appliedFixes": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "warnings": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "droppedSteps": {
                          "type": "array",
                          "description": "For a waterfall: the template's steps this column does not have. Omitted when none were left out.",
                          "items": {
                            "$ref": "#/components/schemas/DroppedStep"
                          }
                        },
                        "verification": {
                          "$ref": "#/components/schemas/VerificationReport"
                        },
                        "ignoredKeys": {
                          "$ref": "#/components/schemas/IgnoredKeys"
                        },
                        "created": {
                          "type": "array",
                          "description": "Only for a batch: the created columns, in the order sent, each with the fixes applied and keys not read on its way in. A batch answers no top-level `column`; its top-level `appliedFixes` and `warnings` are table-wide and name their column.",
                          "items": {
                            "type": "object",
                            "required": ["column"],
                            "properties": {
                              "column": { "$ref": "#/components/schemas/FunctionStepColumn" },
                              "appliedFixes": { "type": "array", "items": { "type": "string" } },
                              "ignoredKeys": { "$ref": "#/components/schemas/IgnoredKeys" }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The body is not what `POST /v1/tables/{tableId}/columns` accepts, or the step is not one the API can configure yet: the error names the task and lists what is allowed. In a batch the error names the entry as `columns[i]`, and nothing is stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "description": "No function with this id in this workspace. A function in another workspace, one in Trash, an id that names nothing, and the id of a table or of a function's body all answer the same. On a step route, also: no column with this id in the sandbox.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The key's role is below editor (the body carries `requiredRole` and `role`), or the function is owner-only and this key is neither its owner's nor an admin's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "No sandbox is open for this function, so there is nothing these routes may write. The body carries `code: sandbox_required`. Open one with `POST /v1/functions/{functionId}/sandbox` and send this again. A `409` without that code means other writes kept changing the function's steps while this one was being stored, so nothing was stored: send it again.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": ["sandbox_required"]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AccountsUnavailable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/functions/{functionId}/sandbox/columns/{columnId}": {
      "patch": {
        "operationId": "updateFunctionStep",
        "summary": "Update a step in a function's sandbox",
        "description": "Change a step in the function's open sandbox: its name, prompt, settings or output shape. The body is exactly what `PATCH /v1/tables/{tableId}/columns/{columnId}` takes, checked by the same code. The step must be one the API can configure (AI columns, and the tasks `icypeasFindEmail`, `hunterFindEmail`, `waterfallEmail`, `formula`, the twelve PredictLeads actions (`predictLeadsEnrichCompany`, `predictLeadsEnrichCompanyExtended`, `predictLeadsFindConnections`, `predictLeadsFindFinancing`, `predictLeadsFindJobs`, `predictLeadsFindLookalikes`, `predictLeadsFindNews`, `predictLeadsFindTechStack`, `predictLeadsGithubRepos`, `predictLeadsProducts`, `predictLeadsSecFilings`, `predictLeadsWebsiteSubpages`), the ten AI Ark actions (`aiArkEnrichPerson`, `aiArkFindWorkEmail`, `aiArkPersonalityProfile`, `aiArkFindMobilePhone`, `aiArkReverseEmailLookup`, `aiArkEnrichCompany`, `aiArkFindLookalikeCompanies`, `aiArkFindPeopleAtCompany`, `aiArkCountPeople`, `aiArkFindRecentlyJoined`), `clearoutPhoneValidatePhone`, `reverseContactDomainToCompany`, the six Loopster Data actions (`loopsterDataFindWorkEmail`, `loopsterDataFindPersonalEmail`, `loopsterDataFindPhoneNumber`, `loopsterDataEnrichPerson`, `loopsterDataEmailToLinkedin`, `loopsterDataEnrichCompany`), the nine Aviato actions (`aviatoFindPersonalEmail`, `aviatoFindHashedEmail`, `aviatoReverseEmailLookup`, `aviatoNormalizeLocation`, `aviatoCompanyFinancials`, `aviatoCompanyWebTraffic`, `aviatoCompanyLegal`, `aviatoCompanyAcquisitions`, `aviatoCompanySocialMetrics`), and these actions, which answer in the background while the function waits: the three Wiza actions (`wizaFindWorkEmail`, `wizaFindMobilePhone`, `wizaFindProfileDetails`), the four FullEnrich actions (`fullEnrichFindWorkEmail`, `fullEnrichFindPersonalEmail`, `fullEnrichFindMobilePhone`, `fullEnrichReverseEmailLookup`), the two BetterContact actions (`betterContactFindWorkEmail`, `betterContactFindMobilePhone`), `reverseContactFindWorkEmail`, and the six Surfe actions (`surfeFindWorkEmail`, `surfeFindPersonalEmail`, `surfeFindMobilePhone`, `surfeFindProfile`, `surfeReverseEmailLookup`, `surfeEnrichCompany`)), before the change and after it; an input or the send-back column is not a step and is refused. Without an open sandbox this answers 409, and a function's live body is never written. Nobody's runs see the change until the sandbox is published. Needs an editor key. The step in the answer carries `costEstimate`: what one run of it costs.",
        "tags": ["Functions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/functionId"
          },
          {
            "$ref": "#/components/parameters/columnId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "header": {
                    "type": "string",
                    "minLength": 1
                  },
                  "type": {
                    "type": "string",
                    "enum": ["manual", "ai", "api", "task", "reference"]
                  },
                  "dataShape": {
                    "type": "string",
                    "description": "One of the data shapes the product defines; see the dataShape returned by GET /columns. An unrecognised value is rejected with 400 rather than stored. `people-table-link` is set by Loopster, on a company table's People search column, and is refused with 400 as a change; sent back as the column's current dataShape, it is accepted and changes nothing."
                  },
                  "order": {
                    "type": "number"
                  },
                  "description": {
                    "type": "string"
                  },
                  "dependencies": {
                    "type": "array",
                    "description": "Array of column IDs (strings) or objects with columnId and required fields. Without required, a column the configuration reads takes the required it reads it with, and any other is required, except on a waterfall column, where it defaults to false: a waterfall waits for that column without skipping a row that has none, as it does for its own inputs. One the column's configuration does not read, or sent with a required it would not give, is kept when the configuration later changes; one that matches what it reads follows it.",
                    "items": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "columnId": {
                              "type": "string"
                            },
                            "required": {
                              "type": "boolean"
                            }
                          },
                          "required": ["columnId"]
                        }
                      ]
                    }
                  },
                  "aiConfig": {
                    "type": "object",
                    "description": "AI column configuration (for type=ai). Merged into the stored configuration; send null for a field to remove it. `model` must be a registered model id and every entry in `tools` a registered tool, or the request is rejected with 400 naming the valid ones. Unknown fields are rejected too \u2014 a misspelt setting used to be stored and silently do nothing. `account` chooses who pays: `\"loopster\"` runs on Loopster's account and bills your wallet, `\"user\"` runs on your own OpenRouter key (choose which one with `integrationConnectionId`). `\"fluar\"`, an older name for `\"loopster\"`, is still accepted and is stored and returned as `\"loopster\"`."
                  },
                  "apiConfig": {
                    "type": "object",
                    "description": "API column configuration (for type=api). Merged into the stored configuration; send null for a field to remove it."
                  },
                  "taskConfig": {
                    "type": "object",
                    "description": "Task column configuration (for type=task), of the form { taskType, [taskType]: { \u2026 } }. Merged into the stored configuration; send null for a field to remove it. The task type and the configuration under it are both checked against the task registry, so a column that could never run is rejected here rather than failing on every row."
                  },
                  "referenceConfig": {
                    "type": "object",
                    "description": "Reference column configuration (for type=reference). Merged into the stored configuration; send null for a field to remove it."
                  },
                  "integrationConnectionId": {
                    "type": "string",
                    "format": "uuid",
                    "nullable": true,
                    "description": "Which of the workspace's connections a task column runs on: an id from GET /v1/connections, for the provider the column's task uses. Send null to clear it. Rejected with 400, naming the connections that would work, when the id is not a connected account of that provider this workspace can use, when the column is not a task column, when its task takes no connection, or when the same request sets execution_mode to \"managed\". Choosing a connection takes a column off Loopster's own account, as the column settings do, and appliedFixes says so."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Column updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "childColumnsBuilt": { "$ref": "#/components/schemas/BuiltChildColumns" },
                        "column": {
                          "$ref": "#/components/schemas/FunctionStepColumn"
                        },
                        "appliedFixes": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "warnings": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "ignoredKeys": {
                          "$ref": "#/components/schemas/IgnoredKeys"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The body carries nothing this endpoint applies or a setting it refuses, or the column is not a step the API can configure: an input, the send-back column, or a task outside the allowlist. Nothing is stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "description": "No function with this id in this workspace. A function in another workspace, one in Trash, an id that names nothing, and the id of a table or of a function's body all answer the same. On a step route, also: no column with this id in the sandbox.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The key's role is below editor (the body carries `requiredRole` and `role`), or the function is owner-only and this key is neither its owner's nor an admin's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "No sandbox is open for this function, so there is nothing these routes may write. The body carries `code: sandbox_required`. Open one with `POST /v1/functions/{functionId}/sandbox` and send this again. A `409` without that code means other writes kept changing the function's steps while this one was being stored, so nothing was stored: send it again. With `code: waterfall_running`, this waterfall has rows running, and giving it its result column now would make those rows run, and be charged, again. Nothing was changed; send it again when they have finished. `runningCells` says how many are still running.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": ["sandbox_required", "waterfall_running"]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "operationId": "deleteFunctionStep",
        "summary": "Delete a step from a function's sandbox",
        "description": "Remove a step from the function's open sandbox, along with the sandbox's values for it. An input or the send-back column is not a step and is refused: remove an input with `DELETE /v1/functions/{functionId}/inputs/{inputId}`. If the step was one of the function's outputs, set the outputs again before publishing. Without an open sandbox this answers 409, and a function's live body is never written. Needs an editor key.",
        "tags": ["Functions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/functionId"
          },
          {
            "$ref": "#/components/parameters/columnId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Column deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "deletedColumnId": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The column is not a step the API can remove: an input, the send-back column, or a task outside the allowlist. Nothing is removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "404": {
            "description": "No function with this id in this workspace. A function in another workspace, one in Trash, an id that names nothing, and the id of a table or of a function's body all answer the same. On a step route, also: no column with this id in the sandbox.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The key's role is below editor (the body carries `requiredRole` and `role`), or the function is owner-only and this key is neither its owner's nor an admin's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "No sandbox is open for this function, so there is nothing these routes may write. The body carries `code: sandbox_required`. Open one with `POST /v1/functions/{functionId}/sandbox` and send this again. A `409` without that code means other writes kept changing the function's steps while this one was being stored, so nothing was stored: send it again.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": ["sandbox_required"]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/functions/{functionId}/inputs": {
      "post": {
        "operationId": "addFunctionInput",
        "summary": "Add a function input",
        "description": "Declare an input, in the open sandbox. Needs an editor key. `required` has no default: whether a new input is required decides whether publishing fails every row of every existing caller, so it is yours to state. The stored `header` comes back, and may carry a suffix that makes it unique. The input reaches callers when the sandbox is published.",
        "tags": ["Functions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/functionId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["header", "required"],
                "additionalProperties": false,
                "properties": {
                  "header": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 128
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "required": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Input added",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "input": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "header": {
                              "type": "string"
                            },
                            "description": {
                              "type": "string"
                            },
                            "required": {
                              "type": "boolean"
                            }
                          }
                        },
                        "function": {
                          "$ref": "#/components/schemas/FunctionDetail"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid: no header, no `required`, or an unknown field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "description": "The key's role is below what this needs (the body carries `requiredRole` and `role`), or the function is owner-only and this key is neither its owner's nor an admin's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No function with this id in this workspace. A function in another workspace, one in Trash, an id that names nothing and the id of anything that is not a function all answer the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "No sandbox is open (`code: sandbox_required`). Inputs and outputs change only inside a sandbox; open one with POST /v1/functions/{functionId}/sandbox.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": ["sandbox_required"]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/functions/{functionId}/inputs/{inputId}": {
      "patch": {
        "operationId": "updateFunctionInput",
        "summary": "Update a function input",
        "description": "Rename an input, describe it, or change whether it is required, in the open sandbox. Needs an editor key. A field left out is left alone; `description: null` clears it.",
        "tags": ["Functions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/functionId"
          },
          {
            "name": "inputId",
            "in": "path",
            "required": true,
            "description": "The input's id, as `draft.inputs[].id` lists it.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "minProperties": 1,
                "properties": {
                  "header": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 128
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 500,
                    "nullable": true
                  },
                  "required": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Input updated",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "function": {
                          "$ref": "#/components/schemas/FunctionDetail"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The body is empty, or carries an unknown field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "description": "The key's role is below what this needs (the body carries `requiredRole` and `role`), or the function is owner-only and this key is neither its owner's nor an admin's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No function with this id in this workspace, or the function has no input with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "No sandbox is open (`code: sandbox_required`), or another column on the function already has that name.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": ["sandbox_required"]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "operationId": "removeFunctionInput",
        "summary": "Remove a function input",
        "description": "Remove an input from the open sandbox. Needs an editor key. Callers keep the contract they have until the sandbox is published.",
        "tags": ["Functions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/functionId"
          },
          {
            "name": "inputId",
            "in": "path",
            "required": true,
            "description": "The input's id, as `draft.inputs[].id` lists it.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Input removed",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "function": {
                          "$ref": "#/components/schemas/FunctionDetail"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "description": "The key's role is below what this needs (the body carries `requiredRole` and `role`), or the function is owner-only and this key is neither its owner's nor an admin's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No function with this id in this workspace, or the function has no input with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "No sandbox is open (`code: sandbox_required`). Inputs and outputs change only inside a sandbox; open one with POST /v1/functions/{functionId}/sandbox.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": ["sandbox_required"]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/functions/{functionId}/outputs": {
      "put": {
        "operationId": "setFunctionOutputs",
        "summary": "Set a function's outputs",
        "description": "Say what the function returns: the whole list of columns whose results go back to the caller, in the open sandbox. Needs an editor key. The whole list rather than a change to it, so send every column you want returned. The ids are the `draft.outputs[].columnId` values GET /v1/functions/{functionId} lists. An id that names nothing the function can return is refused by name rather than dropped.",
        "tags": ["Functions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/functionId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["columnIds"],
                "additionalProperties": false,
                "properties": {
                  "columnIds": {
                    "type": "array",
                    "maxItems": 200,
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Outputs set",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "function": {
                          "$ref": "#/components/schemas/FunctionDetail"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid, or an id names nothing the function can return; the error names the ids.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "description": "The key's role is below what this needs (the body carries `requiredRole` and `role`), or the function is owner-only and this key is neither its owner's nor an admin's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No function with this id in this workspace. A function in another workspace, one in Trash, an id that names nothing and the id of anything that is not a function all answer the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "No sandbox is open (`code: sandbox_required`), or the function has no Send data back step to return anything through.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": ["sandbox_required"]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/functions/{functionId}/publish": {
      "post": {
        "operationId": "publishFunction",
        "summary": "Publish a function",
        "description": "Make what the function's draft describes the contract callers run. With a sandbox open, that is the sandbox, and its steps become the live ones. A live publish needs an **admin** key, because it changes a contract other tables and agents may already be calling. `{\"dryRun\": true}` needs only an editor key: it answers with the contract that would go live and the refusal it would meet, and changes nothing. With a sandbox open it reads the sandbox as the publish would leave it, its waterfall steps' columns built, so it names the outputs the publish returns and the refusals building those columns would meet. Every publish is recorded with who made it and the contract before and after.",
        "tags": ["Functions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/functionId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "dryRun": {
                    "type": "boolean",
                    "description": "Report what a publish would do without doing it. An editor key may send it."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Published, or for a dry run what a publish would do. A dry run always answers 200, with the refusal it would meet in `refusal`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "childColumnsBuilt": { "$ref": "#/components/schemas/BuiltChildColumns" },
                        "published": {
                          "type": "boolean",
                          "description": "True after a live publish. Absent from a dry run."
                        },
                        "function": {
                          "$ref": "#/components/schemas/FunctionDetail"
                        },
                        "dryRun": {
                          "type": "boolean",
                          "description": "True in a dry run's answer."
                        },
                        "wouldPublish": {
                          "type": "boolean",
                          "description": "Dry run: whether a live publish would go through."
                        },
                        "contract": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/FunctionContract"
                            }
                          ],
                          "description": "Dry run: the contract that would go live."
                        },
                        "refusal": {
                          "type": "object",
                          "nullable": true,
                          "description": "Dry run: the refusal a live publish would meet, or null.",
                          "properties": {
                            "status": {
                              "type": "integer"
                            },
                            "code": {
                              "type": "string"
                            },
                            "error": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The body carries a field this does not take.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "description": "The key's role is below what this needs (the body carries `requiredRole` and `role`), or the function is owner-only and this key is neither its owner's nor an admin's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No function with this id in this workspace. A function in another workspace, one in Trash, an id that names nothing and the id of anything that is not a function all answer the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "The publish is refused, and `code` says why. `no_outputs`: the function returns nothing; choose outputs and publish again, there is no override. `live_body_changed`: the function's own table was edited in Loopster after this sandbox was opened, and publishing would discard those edits; `lostInputs` names the inputs that would go, and `remedy` says what to send to publish anyway. `nothing_to_publish`: callers already run this contract. `step_columns_missing`: there is no sandbox, and a waterfall step holds a step that answers in the background (Surfe, Wiza, FullEnrich or BetterContact) without the column it reports into; open a sandbox, which builds those columns, then publish. `waterfall_running`: a waterfall step in the sandbox has rows running, and giving it those columns now would make those rows run, and be charged, again; publish again when they have finished. `column_limit`: the columns the publish would build for its waterfall steps take the sandbox past the workspace's column limit. `sandbox_changed`: the sandbox kept changing while it was being published; send it again.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": ["no_outputs", "live_body_changed", "nothing_to_publish", "step_columns_missing", "waterfall_running"]
                        },
                        "lostInputs": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "With `live_body_changed`: the headers of the inputs publishing would discard."
                        },
                        "remedy": {
                          "type": "object",
                          "description": "With `live_body_changed`: the field to add to the same request to publish anyway."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/functions/{functionId}/agent-access": {
      "put": {
        "operationId": "setFunctionAgentAccess",
        "summary": "Open or close a function to agents",
        "description": "Whether agents may list and run this function over the API and MCP. Needs an **admin** key: it decides who outside the workspace's people may call the function. A function also has to be published and active to be listed by GET /v1/functions. Every change is recorded with who made it.",
        "tags": ["Functions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/functionId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["agentAccess"],
                "additionalProperties": false,
                "properties": {
                  "agentAccess": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Agent access set",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "function": {
                          "$ref": "#/components/schemas/FunctionDetail"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`agentAccess` is not true or false, or the body carries an unknown field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "description": "The key's role is below what this needs (the body carries `requiredRole` and `role`), or the function is owner-only and this key is neither its owner's nor an admin's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No function with this id in this workspace. A function in another workspace, one in Trash, an id that names nothing and the id of anything that is not a function all answer the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/functions/{functionId}/runs": {
      "post": {
        "operationId": "runFunction",
        "summary": "Run a function",
        "description": "Start one run of a function. The run is asynchronous: the response returns immediately with a runId to poll at GET /v1/functions/{functionId}/runs/{runId}. Every step inside the function bills exactly as it would in a table. Each input holds its worst-case cost before it runs and is charged what it really cost, never more; inputs past what the wallet covers are refused, not run, and nothing is charged for them. A run that cannot cover even its first input is refused with 402 `insufficient_balance` before anything is created. The function must be published, active, and enabled for agent access. Send `inputs` as a list to run each input: the ones whose shape is right run, and the rest come back by index in `rejected` with a fixed reason (`wrong_type`, `missing_field`), never echoing a value. A list where none can run is a 400 and starts nothing. A list counts as one start per 100 accepted inputs against the per-minute spending limit, reserved all at once. The answer carries one `runId` for the batch, which `GET /v1/runs/{runId}` reports on, and each accepted input's `rowId`, which `GET .../runs/{rowId}` still answers. A single object answers exactly as before.",
        "tags": ["Functions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/functionId"
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "inputs": {
                    "oneOf": [
                      {
                        "type": "object",
                        "additionalProperties": true,
                        "description": "Input values keyed by input id from the function's input schema. Required inputs must be present and non-empty."
                      },
                      {
                        "type": "array",
                        "minItems": 1,
                        "maxItems": 1000,
                        "items": {
                          "type": "object",
                          "additionalProperties": true
                        },
                        "description": "A list of input objects, each run as its own row (LOOPSTR-2459). At most 1,000, and the body at most 4,000,000 bytes."
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              },
              "X-RateLimit-Spend-Limit": {
                "$ref": "#/components/headers/RateLimitSpendLimit"
              },
              "X-RateLimit-Spend-Remaining": {
                "$ref": "#/components/headers/RateLimitSpendRemaining"
              }
            },
            "description": "Run started",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "runId": {
                          "type": "string",
                          "format": "uuid",
                          "description": "For one input, the row id to poll at `GET /v1/functions/{functionId}/runs/{runId}`, which `GET /v1/runs/{runId}` also answers. For a list, the batch's id for `GET /v1/runs/{runId}`; absent in the rare case the run could not be recorded (then `message` says so, and each row still answers at its own poll URL)."
                        },
                        "status": {
                          "type": "string",
                          "enum": ["running"]
                        },
                        "accepted": {
                          "type": "integer",
                          "description": "For a list: how many inputs the queue took and are running. An input written but refused by the queue is in `notStarted`, not here."
                        },
                        "items": {
                          "type": "array",
                          "description": "For a list: each input the queue took, with its index and the row it runs as.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "index": {
                                "type": "integer",
                                "minimum": 0
                              },
                              "rowId": {
                                "type": "string",
                                "format": "uuid"
                              }
                            }
                          }
                        },
                        "rejected": {
                          "type": "array",
                          "description": "For a list: inputs that could not run, by index, at most the first 100 (see `rejectedCount`, `truncated`).",
                          "items": {
                            "type": "object",
                            "properties": {
                              "index": {
                                "type": "integer",
                                "minimum": 0
                              },
                              "reason": {
                                "type": "string",
                                "enum": ["wrong_type", "missing_field"]
                              }
                            }
                          }
                        },
                        "rejectedCount": {
                          "type": "integer"
                        },
                        "truncated": {
                          "type": "boolean"
                        },
                        "message": {
                          "type": "string"
                        },
                        "notStarted": {
                          "type": "array",
                          "description": "For a list: inputs written but refused by the queue (for example when it is full). They will not run; send them again.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "index": {
                                "type": "integer",
                                "minimum": 0
                              },
                              "rowId": {
                                "type": "string",
                                "format": "uuid"
                              }
                            }
                          }
                        },
                        "notStartedCount": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid. For a list of inputs where none can run, the body also lists every input by index with the reason (`wrong_type`: not an object; `missing_field`: a required input is missing), never echoing a value, and nothing was started.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success", "error"],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [false]
                    },
                    "error": {
                      "type": "string"
                    },
                    "rejected": {
                      "type": "array",
                      "description": "For a list: the inputs that could not run, at most the first 100.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "index": {
                            "type": "integer",
                            "minimum": 0
                          },
                          "reason": {
                            "type": "string",
                            "enum": ["wrong_type", "missing_field"]
                          }
                        }
                      }
                    },
                    "rejectedCount": {
                      "type": "integer"
                    },
                    "truncated": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "description": "The function is not enabled for agent access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The function cannot accept runs right now \u2014 it is a draft, paused, or stopped, or (`code: step_retired`, with `retiredSteps`) a step of it runs a task Loopster no longer runs, so every run would fail. Also answered for an Idempotency-Key that is still in use, was used for a different request, or whose first request never finished; `code` says which (see Idempotent Requests).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 4,000,000 bytes. Nothing was started; split the list into smaller calls.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Loopster could not hand the run to its workers, and the request to them was never sent: nothing was started, nothing was charged, and the rows created for this run were removed. Send the same request again after `Retry-After` seconds; with an `Idempotency-Key`, the same key, since this answer is not stored against it. A `500` makes no such promise: the run may have been created, so retry it with a new key. The body's `code` is `nothing_started`. The same answer when the workers' queue was full, with their wait in `Retry-After` (at most 120 seconds). A `500` with `code: \"nothing_started_internal\"` also means nothing was started (and for a run, its rows were removed), but the workers refused the request itself, a fault on Loopster's side: sending it again now will likely fail the same way.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/functions/{functionId}/runs/{runId}": {
      "get": {
        "operationId": "getFunctionRun",
        "summary": "Get a function run",
        "description": "Poll a run started by POST /v1/functions/{functionId}/runs. The run is finished when the function's return step has settled, so a run whose output step was never dispatched still reaches a terminal status rather than reporting \"running\" forever. \"complete\" carries the outputs keyed as the function publishes them; \"failed\" means at least one output step errored, and errors lists which, or the return step itself failed, in which case dispatch says so and why. A run invoked through this API has no calling table, so it never reports a dispatch failure on that account alone. An output whose step was skipped has an empty value and is named in skipped with the reason, on a run that is still complete. Every message here that a cell stored (a skipped output's reason, a failed output's message, the reason the return step failed) has machine output replaced by a standard sentence and any credential removed. Finished runs the function's retention has trimmed are answered from the run archive with archived: true.",
        "tags": ["Functions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/functionId"
          },
          {
            "$ref": "#/components/parameters/runId"
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Run status retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "runId": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "status": {
                          "type": "string",
                          "enum": ["running", "complete", "failed"]
                        },
                        "outputs": {
                          "type": "object",
                          "additionalProperties": true,
                          "description": "Output values keyed by output key. Present once the run has settled."
                        },
                        "failedSteps": {
                          "type": "array",
                          "description": "Steps that are not outputs and failed, with their messages. When such a step is what the outputs and the return step waited on, its error is the reason for the run's.",
                          "items": { "type": "object", "properties": { "step": { "type": "string" }, "message": { "type": "string" } } }
                        },
                        "errors": {
                          "type": "array",
                          "description": "Present when status is failed: the outputs whose step errored.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "key": {
                                "type": "string"
                              },
                              "message": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "dispatch": {
                          "type": "object",
                          "description": "Present when the function's return step itself failed, whoever started the run: on a run started through this API, when an earlier step failed and the return step never ran. Having no calling table to answer is normal for an API run and gives no dispatch.",
                          "properties": {
                            "sent": {
                              "type": "boolean",
                              "enum": [false],
                              "description": "Always false; the key is present only on a failed dispatch."
                            },
                            "reason": {
                              "type": "string",
                              "description": "Why the result never reached the caller."
                            }
                          }
                        },
                        "skipped": {
                          "type": "array",
                          "description": "Present on a settled run when the step behind an output was skipped: never asked, for the reason given. A skipped step settles the run, so the status is still `complete` (or `failed` if another output errored), and the output's value is empty. The usual reason through this API is a step that reads an input's metadata, which a run here cannot supply: those inputs carry `readsMetadata` on the function's contract.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "key": {
                                "type": "string",
                                "description": "The output key."
                              },
                              "reason": {
                                "type": "string",
                                "description": "Why the step did not run, as the step recorded it."
                              }
                            }
                          }
                        },
                        "archived": {
                          "type": "boolean",
                          "description": "True when the run was answered from the run archive because retention trimmed the row."
                        },
                        "hasUnpublishedChanges": {
                          "type": "boolean",
                          "description": "True when the function's body has changes its author has not published. Present on every successful response, including a run answered from the archive. This run used the published contract, so an output key the author expects but does not see here is explained by this flag rather than by anything going wrong in the run. It says nothing about whether the run succeeded."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "description": "The function is not enabled for agent access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/key": {
      "get": {
        "operationId": "getApiKey",
        "summary": "Get the calling API key",
        "description": "The API key this request was made with: its name, role, whether it can start paid work, when it was created, when it expires and when it was last used. Never the key itself. Any role may read its own key. A key created with spending turned off (`canSpend: false`) is refused with 403 `spend_not_allowed` by every call that would start a run: triggering rows (a dry run is allowed), running a function or a capability, creating a table from a starter that runs a paid search or import (a blank or webhook table is allowed), minting a table's webhook address (its URL takes unauthenticated inserts and outlives the key), creating a column schedule or changing one in any way but pausing it (a schedule spends each time it fires, with nobody present), and inserting or editing rows or cells where the table's Auto-run would run a column. A key past `expiresAt` stops working and is refused with 401 `key_expired`. Both are set when the key is created, in Settings → API keys, and cannot be changed afterwards. An MCP OAuth access token is not a key and answers 400.",
        "tags": ["Account"],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Key read",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "$ref": "#/components/schemas/ApiKeyDetails"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/wallet": {
      "get": {
        "operationId": "getWallet",
        "summary": "Get wallet balance",
        "description": "The workspace the API key belongs to, its plan, its wallet balance and its spend limit. `availableUsd` is the same figure the Loopster UI shows, in wallet dollars. Call this before a run to check the balance is enough to pay for it: `balance.spendableUsd` is what decides whether a run starts, not `availableUsd`. ## When a charge becomes visible. A charge is committed BEFORE its cell reaches a terminal status: the worker writes the charge, then writes the cell. So a cell reading `complete`, `unsatisfied` or `skipped` in `GET /v1/tables/{tableId}/status` has already been billed, and `GET /v1/usage` shows that charge \u2014 nothing settles later. The converse does not hold, so poll until the table is idle before reconciling rather than sampling mid-run. One exception: if a run stops between writing a cell's charge and writing the cell, a retry finishes the cell without charging it again, and if the answer still cannot be saved when the run ends, for a reason on Loopster's side, the charge is given back once, as a negative entry called a reversal. It is not given back when the row was cut off at its run deadline, when the worker lost the job partway through, when the run continued in a later job, when a newer run has taken the cell, when the value itself was refused, when it could not be confirmed that the answer was lost, when the charge was made by a run that started before giving charges back began (2026-10-20, about 15:15 UTC), or once the workspace has reached its daily limit of automatic reversals; a run paused for capacity or with a retry left is decided when it finally ends, so a charge against a cell reading `error` is still possible. A completed cell with no charge against it is normal, not a lag: a repeat lookup inside a provider cache window is free, a pay-on-result provider that found nothing is free, and an unforced run does not re-run a cell that already held an answer. Use `POST /v1/tables/{tableId}/trigger?dryRun=true` to see which rows would actually execute before paying for them. This balance is scoped to the ORGANIZATION while `GET /v1/usage` is scoped to the WORKSPACE, so sibling workspaces share this balance but not that breakdown. Both are net of refunds, which are negative rows in the same signed ledger. When the two disagree, `/v1/usage` is the record of what was charged.",
        "tags": ["Account"],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Wallet read",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "$ref": "#/components/schemas/Wallet"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Get usage and spend",
        "description": "What this workspace spent over a window, bucketed by table, column or day. Every figure is the price charged to you, in dollars. Use it to answer what a run cost after it finished. ## When a charge becomes visible. A charge is committed BEFORE its cell reaches a terminal status: the worker writes the charge, then writes the cell. So a cell reading `complete`, `unsatisfied` or `skipped` in `GET /v1/tables/{tableId}/status` has already been billed, and `GET /v1/usage` shows that charge \u2014 nothing settles later. The converse does not hold, so poll until the table is idle before reconciling rather than sampling mid-run. One exception: if a run stops between writing a cell's charge and writing the cell, a retry finishes the cell without charging it again, and if the answer still cannot be saved when the run ends, for a reason on Loopster's side, the charge is given back once, as a negative entry called a reversal. It is not given back when the row was cut off at its run deadline, when the worker lost the job partway through, when the run continued in a later job, when a newer run has taken the cell, when the value itself was refused, when it could not be confirmed that the answer was lost, when the charge was made by a run that started before giving charges back began (2026-10-20, about 15:15 UTC), or once the workspace has reached its daily limit of automatic reversals; a run paused for capacity or with a retry left is decided when it finally ends, so a charge against a cell reading `error` is still possible. A completed cell with no charge against it is normal, not a lag: a repeat lookup inside a provider cache window is free, a pay-on-result provider that found nothing is free, and an unforced run does not re-run a cell that already held an answer. Use `POST /v1/tables/{tableId}/trigger?dryRun=true` to see which rows would actually execute before paying for them. This endpoint is scoped to the WORKSPACE the key belongs to, while `GET /v1/wallet` reports the ORGANIZATION balance, so the balance can move by more than this reports. Amounts are net of refunds, which are negative rows in the same signed ledger. A reversal is not an entry: the reversed charge still counts as one, so `totalEntries` and a bucket's `entries` do not grow by it, and it is not counted in `freeEntries` (it was charged and given back, not free). It is dated when it was given back, usually minutes after its charge, so a window or a day bucket that holds a reversal without its charge can show a negative `usd` with no entry for it; totals over a window holding both are exact.",
        "tags": ["Account"],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Start of the window, inclusive. ISO 8601. Defaults to 30 days ago."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "End of the window, exclusive. ISO 8601. Defaults to now."
          },
          {
            "name": "groupBy",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["table", "column", "day"],
              "default": "table"
            },
            "description": "How to bucket the spend."
          },
          {
            "name": "tableId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Restrict the answer to one table."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            },
            "description": "Maximum buckets to return. The total always covers the whole window regardless of this."
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Scope": {
                "$ref": "#/components/headers/RateLimitScope"
              }
            },
            "description": "Usage read",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "$ref": "#/components/schemas/Usage"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "RetryAfter": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "RateLimitLimit": {
        "description": "The size of the window named by `X-RateLimit-Scope`. On a success it describes the workspace's general budget, whichever of its two windows (per minute, per day) is closer to running out. On a 429 it describes the window that refused. Absent when the window could not be measured.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Requests left in that window, counting this one as used. Absent when the window could not be measured.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "RateLimitScope": {
        "description": "Which window the other two headers describe. `minute` and `day` are the workspace's general budget; `credit-spend` is the window for calls that can start paid work; `exclusion-list` is the window for searches that read a table as an exclusion list; `unauthenticated` and `invalid-credential` count per calling address and appear only on a 429.",
        "schema": {
          "type": "string",
          "enum": ["minute", "day", "credit-spend", "unauthenticated", "invalid-credential", "exclusion-list"]
        }
      },
      "RateLimitSpendLimit": {
        "description": "The window for calls that can start paid work (60 per minute). Present only when this call reserved it: a trigger that is not a dry run, or an insert or webhook into a table with autoplay on, for example.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitSpendRemaining": {
        "description": "Calls that can start paid work left in that window, counting this one as used. Present only when this call reserved it.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key as Bearer token. Get your API key from your team settings in Loopster."
      }
    },
    "parameters": {
      "columnId": {
        "name": "columnId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "The column ID (col_XXXX format)."
      },
      "rowId": {
        "name": "rowId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "The row ID."
      },
      "viewId": {
        "name": "viewId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "The view ID."
      },
      "shareId": {
        "name": "shareId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "Share link id, from the list or create response"
      },
      "tableId": {
        "name": "tableId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "The table ID, from List Tables. Must be a UUID: anything else is refused with `400` before any lookup happens, rather than reported as a missing table."
      },
      "functionId": {
        "name": "functionId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "The function ID."
      },
      "runId": {
        "name": "runId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "The run ID returned by the run endpoint."
      },
      "webhookSecret": {
        "name": "secret",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "The secret from the table's webhook URL. This is a live credential: anyone holding it can add rows to that table, so treat it the way you would an API key and regenerate it if it leaks."
      },
      "onOversize": {
        "name": "onOversize",
        "in": "query",
        "required": false,
        "description": "What to do with a value over its column's size limit (8 kB for typed text, 100 kB for references, 200 kB for enrichment output). `clear` (the default) stores it empty and reports it in `cappedCells`. `reject` writes nothing in the request and answers 422, naming every value that was over.",
        "schema": {
          "type": "string",
          "enum": ["clear", "reject"],
          "default": "clear"
        }
      },
      "idempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_.:-]{1,128}$"
        },
        "description": "A key you choose for this one action, so that retrying it is safe. A repeat of the same request with the same key, within 24 hours, returns the first answer (its status and body, plus `\"replayed\": true` and an `Idempotent-Replayed: true` header) and runs nothing again. A stored failure replays as the same failure; send a new key to try again. A refusal that happened before any work started (a rate limit, a validation error) is not kept. See Idempotent Requests."
      }
    },
    "schemas": {
      "WebhookDeliveryLogEntry": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid", "description": "The delivery id, sent as `Loopster-Delivery-Id`." },
          "event": { "type": "string", "enum": ["run.settled", "ping"] },
          "runId": { "type": "string", "format": "uuid", "nullable": true },
          "status": {
            "type": "string",
            "enum": ["pending", "delivered", "failed", "blocked"],
            "description": "`pending`: waiting for its next attempt. `blocked`: the destination was refused, never retried."
          },
          "attempt": { "type": "integer" },
          "responseStatus": { "type": "integer", "nullable": true },
          "responseContentType": { "type": "string", "nullable": true },
          "responseBytes": { "type": "integer", "nullable": true },
          "durationMs": { "type": "integer", "nullable": true },
          "errorClass": { "type": "string", "nullable": true },
          "createdAt": { "type": "string", "format": "date-time" },
          "nextAttemptAt": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "WebhookEndpoint": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "host": { "type": "string", "description": "The URL's host." },
          "urlHint": {
            "type": "string",
            "description": "Scheme, host and at most the last four characters of the path, e.g. `https://hooks.zapier.com/…KEN/`. Never the whole URL."
          },
          "secretLastFour": { "type": "string" },
          "events": { "type": "array", "items": { "type": "string", "enum": ["run.settled"] } },
          "description": { "type": "string", "nullable": true },
          "disabled": {
            "type": "boolean",
            "description": "True when deliveries kept failing and the endpoint was turned off. Delete it and create it again to start over."
          },
          "disabledAt": { "type": "string", "format": "date-time", "nullable": true },
          "disabledReason": { "type": "string", "nullable": true },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "deliveryId": { "type": "string", "format": "uuid", "description": "Also sent as the `Loopster-Delivery-Id` header." },
          "status": {
            "type": "string",
            "enum": ["delivered", "failed", "blocked"],
            "description": "`blocked`: the destination was refused before anything was sent, and is never retried."
          },
          "responseStatus": { "type": "integer" },
          "responseContentType": { "type": "string" },
          "responseBytes": { "type": "integer" },
          "durationMs": { "type": "integer" },
          "errorClass": {
            "type": "string",
            "description": "`timeout`, `network`, `unsafe_destination`, `invalid_url`, `too_large` or `http_<code>`."
          }
        }
      },
      "CreateColumnRequest": {
        "type": "object",
        "required": ["templateId"],
        "properties": {
          "templateId": {
            "type": "string",
            "description": "ID of the column template to use."
          },
          "header": {
            "type": "string",
            "description": "Custom header name. Defaults to the template's `defaultHeader`: the name the Add column menu shows for it, such as `Mobile phone` or `Work email waterfall`. When the table already has a column by the default name, the new one is numbered (`Mobile phone 2`). A header you send must be text, not blank and at most 255 characters, or the call is refused with 400; it is kept as sent."
          },
          "integrationConnectionId": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "For a task template: which of the workspace's connections the column runs on, an id from GET /v1/connections. Judged as on PATCH /columns/{columnId}. Omit it, or send null, to start on the workspace's saved default account for its provider (Loopster's included, billed to your wallet; appliedFixes says so), else on the workspace's own key; it never starts on Loopster's account unless that is the saved default or the action runs only on Loopster's account."
          },
          "type": {
            "type": "string",
            "enum": ["manual", "ai", "api", "task", "reference"]
          },
          "dataShape": {
            "type": "string",
            "description": "One of the data shapes the product defines; see the dataShape returned by GET /columns. An unrecognised value is rejected with 400 rather than stored. `people-table-link` is set by Loopster, on a company table's People search column, and is refused here with 400."
          },
          "order": {
            "type": "number"
          },
          "description": {
            "type": "string"
          },
          "dependencies": {
            "type": "array",
            "description": "Array of column IDs (strings) or objects with columnId and required fields. Without required, a column the configuration reads takes the required it reads it with, and any other is required, except on a waterfall column, where it defaults to false: a waterfall waits for that column without skipping a row that has none, as it does for its own inputs. One the column's configuration does not read, or sent with a required it would not give, is kept when the configuration later changes; one that matches what it reads follows it.",
            "items": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "object",
                  "properties": {
                    "columnId": {
                      "type": "string"
                    },
                    "required": {
                      "type": "boolean"
                    }
                  },
                  "required": ["columnId"]
                }
              ]
            }
          },
          "aiConfig": {
            "type": "object",
            "description": "AI column configuration (for type=ai). Merged into the stored configuration; send null for a field to remove it. `model` must be a registered model id and every entry in `tools` a registered tool, or the request is rejected with 400 naming the valid ones. Unknown fields are rejected too — a misspelt setting used to be stored and silently do nothing. `account` chooses who pays: `\"loopster\"` runs on Loopster's account and bills your wallet, `\"user\"` runs on your own OpenRouter key (choose which one with `integrationConnectionId`). `\"fluar\"`, an older name for `\"loopster\"`, is still accepted and is stored and returned as `\"loopster\"`."
          },
          "apiConfig": {
            "type": "object",
            "description": "API column configuration (for type=api). Merged into the stored configuration; send null for a field to remove it."
          },
          "taskConfig": {
            "type": "object",
            "description": "Task column configuration (for type=task), of the form { taskType, [taskType]: { … } }. Merged into the stored configuration; send null for a field to remove it. The task type and the configuration under it are both checked against the task registry, so a column that could never run is rejected here rather than failing on every row."
          },
          "referenceConfig": {
            "type": "object",
            "description": "Reference column configuration (for type=reference). Merged into the stored configuration; send null for a field to remove it."
          }
        }
      },
      "Wallet": {
        "type": "object",
        "properties": {
          "workspace": {
            "type": "object",
            "description": "The workspace this API key belongs to.",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string",
                "nullable": true
              },
              "slug": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "plan": {
            "type": "object",
            "properties": {
              "key": {
                "type": "string",
                "description": "Stable plan identifier, e.g. free, launch, scale."
              },
              "name": {
                "type": "string",
                "description": "Plan name as shown in the product."
              },
              "activeSubscription": {
                "type": "boolean"
              }
            }
          },
          "balance": {
            "type": "object",
            "description": "Wallet figures in US dollars, at the price charged to you. The wallet belongs to the account, so sibling workspaces under one organization share it.",
            "properties": {
              "availableUsd": {
                "type": "number",
                "description": "The wallet balance. This is the number the wallet pill in the product shows. It is not what decides whether a run starts: read spendableUsd for that."
              },
              "grantedUsd": {
                "type": "number",
                "description": "Everything ever added to the wallet."
              },
              "consumedUsd": {
                "type": "number",
                "description": "Everything ever spent from it."
              },
              "spendableUsd": {
                "type": "number",
                "description": "What the next run can draw on: the balance less deficitUsd and less heldUsd, or, for an account with no subscription, what is left of its free allowance. This is the number that decides whether a run starts. Every paid unit of work holds its worst-case cost from it before it runs, so a run larger than spendableUsd runs the rows it covers, in order, and the rest are refused rather than overdrawing the wallet."
              },
              "heldUsd": {
                "type": "number",
                "description": "Money held right now for work in flight. Each hold is the most that work can cost; when it finishes it is charged what it really cost, never more than it held, and the rest is released."
              },
              "deficitUsd": {
                "type": "number",
                "description": "Money already spent that the wallet could not cover. The next top-up repays it first, and it is already subtracted from spendableUsd."
              },
              "expiringSoonUsd": {
                "type": "number",
                "description": "Balance that expires at nextExpirationAt."
              },
              "nextExpirationAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "computedAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "spendLimit": {
            "type": "object",
            "description": "This workspace's own cap on drawing from the shared wallet.",
            "properties": {
              "state": {
                "type": "string",
                "enum": ["unlimited", "ok", "reached"]
              },
              "limitUsd": {
                "type": "number",
                "nullable": true,
                "description": "Null when no limit is set."
              },
              "spentUsd": {
                "type": "number",
                "nullable": true,
                "description": "Null when no limit is set, because the figure is not computed then. Null means not counted, not zero."
              }
            }
          }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "period": {
            "type": "object",
            "properties": {
              "from": {
                "type": "string",
                "format": "date-time"
              },
              "to": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "groupBy": {
            "type": "string",
            "enum": ["table", "column", "day"]
          },
          "totalUsd": {
            "type": "number",
            "description": "Total spend over the whole window, in dollars, regardless of how many buckets were returned."
          },
          "totalEntries": {
            "type": "integer",
            "description": "Number of billing entries in the window. A reversal, which gives a charge back, is not counted: the charge it reverses still is."
          },
          "totalFreeEntries": {
            "type": "integer",
            "description": "How many of totalEntries ran and cost nothing: a repeat lookup inside the cache window, or a lookup that returned nothing billable. Always part of totalEntries, never of totalUsd. A charge that was given back is not free work and is not counted here."
          },
          "truncated": {
            "type": "boolean",
            "description": "True when there were more buckets than limit allowed. Do not add the items up when this is true; read totalUsd."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UsageBucket"
            }
          }
        }
      },
      "UsageBucket": {
        "type": "object",
        "description": "One bucket of spend. Exactly one of tableId, columnId or date identifies it, decided by groupBy; the others are null. A capability run's spend (grouped by table) names its `templateId` instead, with `tableId` null: its hidden table is never named.",
        "properties": {
          "tableId": {
            "type": "string",
            "nullable": true
          },
          "templateId": {
            "type": "string",
            "description": "For spend on a capability run (LOOPSTR-2458): the capability's template, in place of its hidden table."
          },
          "columnId": {
            "type": "string",
            "nullable": true
          },
          "date": {
            "type": "string",
            "nullable": true,
            "description": "UTC day, YYYY-MM-DD."
          },
          "usd": {
            "type": "number",
            "description": "Spend in this bucket, in dollars, at the price charged to you. Net of reversals, so a bucket that holds a reversal without its charge can be negative."
          },
          "entries": {
            "type": "integer"
          },
          "freeEntries": {
            "type": "integer",
            "description": "How many of this bucket's entries ran and cost nothing. Part of entries, never of usd."
          }
        }
      },
      "SuccessResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          }
        }
      },
      "ApiKeyDetails": {
        "type": "object",
        "description": "An API key's own details and limits. Never the key or its hash.",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "role": { "type": "string", "enum": ["member", "editor", "admin"] },
          "canSpend": {
            "type": "boolean",
            "description": "Whether this key may start paid work. False refuses every call that would start a run with 403 `spend_not_allowed`."
          },
          "createdAt": { "type": "string", "format": "date-time" },
          "expiresAt": {
            "type": "string",
            "nullable": true,
            "format": "date-time",
            "description": "When this key stops working, or null if it never expires."
          },
          "lastUsedAt": { "type": "string", "nullable": true, "format": "date-time" }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "ProjectSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "autoplay": {
            "type": "boolean"
          },
          "columnCount": {
            "type": "integer"
          },
          "folderId": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ProjectDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "columns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Column"
            },
            "description": "Every column with its full configuration, the same shape List Columns returns."
          },
          "autoplay": {
            "type": "boolean"
          },
          "folderId": {
            "type": "string",
            "nullable": true,
            "format": "uuid",
            "description": "The folder the table is in, or null at the top level."
          },
          "settings": {
            "$ref": "#/components/schemas/TableSettings"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TableSettings": {
        "type": "object",
        "description": "The settings the product acts on for this table. Absent stored values answer their default.",
        "required": ["autoplay", "createColumnsFromKeys", "apiEnabled", "autoDedupe", "sandboxOpen"],
        "properties": {
          "autoplay": {
            "type": "boolean",
            "description": "Whether inserted rows run the table's enrichment columns."
          },
          "createColumnsFromKeys": {
            "type": "boolean",
            "description": "Whether a row insert creates a column for each key that matches no column."
          },
          "apiEnabled": {
            "type": "boolean",
            "description": "Whether the table answers the API and its webhook. Changed in the app only; a table with it off answers 403, so a readable value is always true."
          },
          "autoDedupe": {
            "type": "object",
            "required": ["enabled", "columnId", "keep"],
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Whether inserts are deduplicated now. False when the chosen column no longer exists, as inserts then skip it."
              },
              "columnId": {
                "type": "string",
                "nullable": true,
                "description": "The column rows are matched on, or null when none was chosen."
              },
              "keep": {
                "type": "string",
                "enum": ["oldest", "newest"],
                "description": "Which of two matching rows stays."
              }
            }
          },
          "sandboxOpen": {
            "type": "boolean",
            "description": "Whether a sandbox is open on the table. While one is, the table's columns are edited in the sandbox."
          }
        }
      },
      "Column": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "header": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": ["manual", "ai", "api", "task", "reference"]
          },
          "dataShape": {
            "type": "string",
            "description": "The column's data shape. A company table's People search column, added by a Find people at these companies import, reports `people-table-link`; its cells hold the count of people found (for example \"3 people\"). That value is set by Loopster and cannot be given to a column through this API."
          },
          "order": {
            "type": "number"
          },
          "trigger": {
            "type": "boolean"
          },
          "dependencies": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "aiConfig": {
            "type": "object",
            "description": "Present for ai columns. `account` is `loopster` (Loopster's account, billed to the wallet) or `user` (your own key); absent means Loopster's account unless the column names a connection."
          },
          "apiConfig": {
            "type": "object",
            "description": "Present for api columns."
          },
          "taskConfig": {
            "type": "object",
            "description": "Present for task columns."
          },
          "referenceConfig": {
            "type": "object",
            "description": "Present for reference columns."
          },
          "fieldType": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "description": "The column's own description, when it has one."
          },
          "manualTriggerOnly": {
            "type": "boolean",
            "description": "True when the column only runs when asked, never automatically."
          },
          "integrationConnectionId": {
            "type": "string",
            "description": "Which of the workspace's connections the column runs under, an id from GET /v1/connections. Ignored while the column's taskConfig sets execution_mode to \"managed\"; absent means the workspace's default connection for that provider."
          },
          "integrationConnectionName": {
            "type": "string"
          },
          "inputs": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Present for function columns."
          },
          "selectConfig": {
            "type": "object",
            "description": "Permitted values, for a select column."
          },
          "exportConfig": {
            "type": "object"
          },
          "triggerType": {
            "type": "string"
          }
        },
        "description": "A column with its full configuration. Fields a column has never had are omitted rather than returned as null. Returned by both List Columns and Get Table, which use the same serializer."
      },
      "DroppedStep": {
        "type": "object",
        "description": "A step the template has and the created waterfall does not, because this workspace has no account to run it on.",
        "properties": {
          "id": { "type": "string" },
          "taskType": { "type": "string" },
          "provider": { "type": "string", "description": "The provider's name, as the app shows it." },
          "waysBack": {
            "type": "array",
            "description": "Every way to bring the step back. connect_key: connect the workspace's own key, in the app. save_default: save Loopster's account as the provider's default (Set Provider Default). explicit_managed: add the step with executionMode managed. The last two only where Loopster offers an account for the provider.",
            "items": { "type": "string", "enum": ["connect_key", "save_default", "explicit_managed"] }
          }
        }
      },
      "VerificationReport": {
        "type": "object",
        "description": "Verification on the waterfall as created. Present when it is on, or when it was held off for want of an account to verify on.",
        "properties": {
          "enabled": { "type": "boolean" },
          "heldOff": {
            "type": "boolean",
            "description": "True when the template verifies and this column does not, because the workspace has no key for the verifier and has not saved Loopster's account as its default."
          },
          "validatorTaskType": { "type": "string" },
          "provider": { "type": "string" },
          "waysBack": {
            "type": "array",
            "description": "How to make switching it on possible. connect_key: connect the workspace's own key, in the app. save_default: save Loopster's account as the verifier's default (Set Provider Default), where Loopster offers one.",
            "items": { "type": "string", "enum": ["connect_key", "save_default"] }
          }
        }
      },
      "BuiltChildColumns": {
        "type": "array",
        "description": "Columns this request built for a waterfall: the ones the app builds for every waterfall, a result column, which holds the waterfall's answer, and one per step. Built where a waterfall lacks any of them. Columns already there are kept. Omitted when nothing was built.",
        "items": {
          "type": "object",
          "properties": {
            "id": { "type": "string", "description": "The new column's id." },
            "header": { "type": "string" },
            "key": { "type": "string", "description": "`result`, or the task of the step the column reports for." }
          },
          "required": ["id", "header", "key"]
        }
      },
      "ColumnTemplate": {
        "type": "object",
        "properties": {
          "droppedSteps": {
            "type": "array",
            "description": "For a waterfall template: the steps a column created from it in this workspace would leave out, the same answer creation gives. Omitted when none.",
            "items": {
              "$ref": "#/components/schemas/DroppedStep"
            }
          },
          "verification": {
            "$ref": "#/components/schemas/VerificationReport"
          },
          "id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "defaultHeader": {
            "type": "string",
            "description": "What a column created from this template is named when Create Column is sent no `header`: the name the Add column menu shows for it, such as `Mobile phone`, `Work email (from name)` or `Mobile phone waterfall`. The vendor is not part of it."
          },
          "description": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "dataShape": {
            "type": "string"
          },
          "cost": {
            "$ref": "#/components/schemas/TemplateCost"
          },
          "discovery": {
            "$ref": "#/components/schemas/TemplateDiscovery"
          }
        }
      },
      "TemplateDiscovery": {
        "type": "object",
        "description": "Where the template sits in the Add column menu and what it needs: the same record the product's own menu reads. Fields that do not apply are omitted.",
        "properties": {
          "placement": {
            "type": "object",
            "description": "`{ tab, group }` for a menu row, or `{ excluded }` for a template with none.",
            "properties": {
              "tab": { "type": "string", "enum": ["import", "enrich", "tools", "functions", "export"] },
              "group": { "type": "string" },
              "excluded": { "type": "string", "enum": ["column_type", "model_variant", "internal", "unclassified"] }
            }
          },
          "dataPoint": { "type": "string", "description": "For an Enrich template, the data point it finds." },
          "provider": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "name": { "type": "string" },
              "firstParty": { "type": "boolean" }
            }
          },
          "inputs": {
            "type": "object",
            "description": "What a row must hold. Each `anyOf` entry is one complete alternative; `[[]]` means nothing in particular.",
            "properties": {
              "anyOf": { "type": "array", "items": { "type": "array", "items": { "type": "string" } } },
              "optional": { "type": "array", "items": { "type": "string" } }
            }
          },
          "createsRows": { "type": "boolean", "description": "True only when each result becomes a new row." },
          "sideEffect": { "type": "string", "enum": ["none", "reads_own_account", "writes_outside"] },
          "actionKind": { "type": "string", "enum": ["push", "look_up"] },
          "collapseKey": {
            "type": "string",
            "description": "Export only. Templates sharing a key are one vendor's actions in one destination group, shown as one row, such as `heyreach:outbound`."
          },
          "actionLabel": {
            "type": "string",
            "description": "Export only. The action's name under its vendor's row, such as `Get Lead` under HeyReach. Present only where one is declared."
          },
          "displayLabel": { "type": "string" },
          "qualifier": { "type": "string" },
          "note": { "type": "string" },
          "summary": {
            "type": "string",
            "description": "What the template is for, in a few words, where its name alone does not say (`By seniority and department`)."
          },
          "synonyms": { "type": "array", "items": { "type": "string" } },
          "waterfallEligible": { "type": "boolean" },
          "connection": { "type": "string", "enum": ["none", "managed", "own_key", "both", "per_step"] },
          "status": { "type": "string", "enum": ["live", "coming_soon", "legacy"] },
          "package": {
            "type": "string",
            "enum": ["loopster_data", "waterfalls", "formatters", "scrapers", "flow", "loopgents"],
            "description": "The Loopster package the menu also lists the template under, beside the vendors. A way to find it, not a second placement: its tab, provider and price are unchanged. Present only for Loopster’s own templates."
          }
        }
      },
      "TemplateCost": {
        "type": "object",
        "description": "What one row of this template costs, at the wallet's sell rate \u2014 the same figure the product shows a person before running the same template, never the provider's cost to us.\n\nThe key is **omitted entirely** where the concept does not apply: a `manual` column charges nothing for a typed-in value, and an `api` column calls an endpoint you supply, which we cannot price. Omitted rather than reported as zero, because \"not applicable\" and \"measured free\" are different claims.",
        "required": ["kind"],
        "properties": {
          "kind": {
            "type": "string",
            "enum": ["free", "byok", "usage_based", "per_row"],
            "description": "`free` \u2014 Loopster runs it and charges nobody. `byok` \u2014 runs on your own connected account, so nothing leaves the wallet. `usage_based` \u2014 no single figure exists; the steps inside bill as they run. `per_row` \u2014 a real dollar figure, in `usd`."
          },
          "usd": {
            "type": "number",
            "description": "Dollars per row. Present only when `kind` is `per_row`.",
            "example": 0.013
          },
          "approximate": {
            "type": "boolean",
            "description": "Present with `per_row`. True for AI templates, whose token cost depends on the prompt and the row, so this is an estimate at the configured agent tier rather than a fixed price. True for a waterfall too, whose cost depends on which provider answers."
          },
          "maxUsd": {
            "type": "number",
            "description": "Present with `per_row` when a row\u2019s cost is a range rather than one price \u2014 a waterfall. `usd` is then the cheapest outcome (the first provider finds it, verification included) and `maxUsd` the most a row can cost, when every provider runs. Priced for this workspace: only the steps `create_column` would give the waterfall count.",
            "example": 0.553
          },
          "note": {
            "type": "string",
            "description": "One sentence of context for the figure, when there is one to give \u2014 for instance that connecting your own account avoids the charge."
          }
        }
      },
      "RowSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "description": "The row's status, derived from its cells: still working if any cell is, otherwise error if any cell failed, otherwise complete once anything has run, otherwise idle."
          },
          "statusMessage": {
            "type": "string",
            "nullable": true,
            "description": "The first failing cell's message when the row is in error; null otherwise."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "cells": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Map of column ID to cell value. A column whose values live on sub-columns (a waterfall, a group) reports under its own id, valued from its result sub-column."
          },
          "cellStatuses": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Map of column ID to cell status, so an empty value can be told apart from a skipped, failed or never-run one."
          },
          "cellErrors": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Map of column ID to failure message, present only for the cells that carry one."
          }
        }
      },
      "RowDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string"
          },
          "statusMessage": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "cells": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "value": {
                  "type": "string",
                  "nullable": true
                },
                "status": {
                  "type": "string",
                  "nullable": true
                },
                "updatedBy": {
                  "type": "string",
                  "nullable": true
                },
                "errorMessage": {
                  "type": "string",
                  "nullable": true
                },
                "capped": {
                  "$ref": "#/components/schemas/CappedCellDetail"
                },
                "waterfall": {
                  "$ref": "#/components/schemas/WaterfallRunSummary"
                }
              }
            },
            "description": "Map of column ID to cell details. The cell holding a waterfall's answer carries `waterfall` once a run is recorded."
          }
        }
      },
      "WaterfallTrace": {
        "type": "object",
        "description": "One waterfall column's run on one row: what it settled on, what it cost, and every provider it tried.",
        "properties": {
          "columnId": {
            "type": "string",
            "description": "The waterfall column this trace is for."
          },
          "column": {
            "type": "string",
            "description": "That column's header."
          },
          "result": {
            "type": "string",
            "nullable": true,
            "description": "What the chain settled on, as the result cell holds it."
          },
          "status": {
            "type": "string",
            "nullable": true,
            "description": "The result cell's status: `complete`, `unsatisfied`, `skipped` or `error`. `unsatisfied` means a value was found and something chose not to present it \u2014 an unverified candidate, or one the duplicate rule withheld."
          },
          "reason": {
            "type": "string",
            "nullable": true,
            "description": "Why the run ended as it did, when the cell records a sentence."
          },
          "successfulProvider": {
            "type": "string",
            "nullable": true,
            "description": "Task type of the winning step. `shared_pool` means the answer came from the shared contact pool, so no step ran and `attempts` is empty. It is free unless the column verifies its answers and the pooled address had to be checked first; then `cost` is that one check."
          },
          "successfulProviderName": {
            "type": "string",
            "nullable": true,
            "description": "That step's vendor as a person would name it."
          },
          "verification": {
            "type": "object",
            "nullable": true,
            "description": "Whether `result` was verified, as the result cell records it. `null` when there is no result, and on rows enriched before the verdict was recorded.",
            "properties": {
              "state": {
                "type": "string",
                "enum": ["verified", "unverified", "unchecked"],
                "description": "`verified`: the verifier checked the address and the strategy accepted it. `unverified`: every candidate was rejected and the best one was kept. `unchecked`: nobody judged it; `reason` says why."
              },
              "verifier": {
                "type": "string",
                "description": "The verifier's task type, when one was configured. `shared_pool` on an answer served on a verdict another run stored in the shared contact pool: that run's verifier is not named."
              },
              "verdict": {
                "type": "string",
                "description": "The verifier's own verdict, such as `valid`, `invalid` or `catch_all`."
              },
              "score": {
                "type": "number",
                "description": "The verifier's confidence, 0-100, when it reports one."
              },
              "strategy": {
                "type": "string",
                "description": "The strategy the verdict was judged under."
              },
              "reason": {
                "type": "string",
                "enum": ["verification_off", "verifier_unavailable", "not_verifiable", "out_of_date", "shared_pool"],
                "description": "Why the result is `unchecked`. `verifier_unavailable` means the verifier failed and the address was accepted on shape alone."
              },
              "checkedAt": {
                "type": "string",
                "format": "date-time",
                "description": "When the verdict was reached, on an answer from the shared contact pool. A pooled address can be served on a verdict reached by an earlier run, up to 30 days before."
              },
              "source": {
                "type": "string",
                "enum": ["shared_pool"],
                "description": "Present when the verdict belongs to an answer from the shared contact pool rather than to a step of this run."
              },
              "summary": {
                "type": "string",
                "description": "The verdict in one sentence, as the grid shows it."
              }
            },
            "required": ["state", "summary"]
          },
          "attempts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WaterfallAttempt"
            },
            "description": "The chain, in configured order."
          },
          "stoppedOnRepeatedRejection": {
            "type": "object",
            "description": "Present only when the chain ended before its last step because the same rejected address came back from as many providers as the column's repeated-rejection setting allows. The steps it never reached are `skipped` attempts.",
            "properties": {
              "address": {
                "type": "string",
                "description": "The rejected address that kept coming back."
              },
              "providers": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Task types of the steps that returned it, in chain order."
              },
              "reason": {
                "type": "string",
                "description": "The same sentence the result cell shows."
              }
            }
          },
          "cost": {
            "type": "object",
            "properties": {
              "chargedTokens": {
                "type": "number",
                "description": "What the wallet was billed for this run."
              },
              "chargedUsd": {
                "type": "number"
              },
              "attemptedTokens": {
                "type": "number",
                "description": "What the individual steps consumed, plus a check run on a shared-pool address before any step. May exceed `chargedTokens` where only the successful lookup is billed."
              },
              "attemptedUsd": {
                "type": "number"
              }
            }
          }
        }
      },
      "WaterfallRunSummary": {
        "type": "object",
        "description": "The account of a waterfall's run that a row carries on the cell holding its answer: who answered, what each step did and whether the answer was verified. A projection of the row's trace, so the two always agree. The run's cost and each step's own value are on the trace.",
        "properties": {
          "columnId": {
            "type": "string",
            "description": "The waterfall column this is the run of. The answer sits on that column's own cell, or on its result column's cell when the waterfall has a column per step."
          },
          "successfulProvider": {
            "type": "string",
            "nullable": true,
            "description": "Task type of the step that answered. `shared_pool` when the answer came from Loopster's shared contact pool before any step ran: then no step answered, and `attempts` is empty. `null` when the run found nothing."
          },
          "successfulProviderName": {
            "type": "string",
            "nullable": true,
            "description": "That provider as a person would name it."
          },
          "verification": {
            "type": "object",
            "nullable": true,
            "description": "Whether the answer was verified: the same object as `verification` on the trace, with `state` (`verified`, `unverified` or `unchecked`), the verifier and its verdict where there is one, and a `summary` sentence. `null` when there is no answer, and on rows enriched before the verdict was recorded.",
            "properties": {
              "state": {
                "type": "string",
                "enum": ["verified", "unverified", "unchecked"]
              },
              "summary": {
                "type": "string",
                "description": "The verdict in one sentence, as the table shows it."
              }
            }
          },
          "attempts": {
            "type": "array",
            "description": "Each step of the chain in order: `order`, `taskType`, `provider`, `status`, `outcome`, `reason` and `won`, with the same meanings as on the trace. Empty when the answer came from the shared pool.",
            "items": {
              "type": "object",
              "properties": {
                "order": {
                  "type": "integer"
                },
                "taskType": {
                  "type": "string"
                },
                "provider": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "enum": ["complete", "skipped", "error"]
                },
                "outcome": {
                  "type": "string",
                  "description": "The same values as `outcome` on a trace attempt."
                },
                "reason": {
                  "type": "string",
                  "nullable": true
                },
                "won": {
                  "type": "boolean"
                }
              }
            }
          },
          "stoppedOnRepeatedRejection": {
            "type": "object",
            "description": "Present only when the chain ended before its last step on a repeated rejection: the same object as on the trace."
          }
        }
      },
      "WaterfallAttempt": {
        "type": "object",
        "description": "One provider's turn in the chain.",
        "properties": {
          "order": {
            "type": "integer",
            "description": "1-based position in the chain."
          },
          "taskType": {
            "type": "string"
          },
          "provider": {
            "type": "string",
            "description": "The vendor as a person would name it."
          },
          "columnId": {
            "type": "string",
            "description": "The step's own column, when the waterfall has one per step. Read this cell when you need the provider's raw response, which the trace deliberately omits. Absent on a waterfall with no step columns: one made through the API has none unless it holds a step that answers in the background (Surfe, Wiza, FullEnrich or BetterContact), or until it is opened in the app."
          },
          "status": {
            "type": "string",
            "enum": ["complete", "skipped", "error"]
          },
          "outcome": {
            "type": "string",
            "enum": ["found", "no_match", "not_verified", "duplicate", "out_of_date", "wrong_line_type", "not_used", "skipped", "error"],
            "description": "Why this step did not produce the answer, or `found` where it did. `no_match` is the provider having nobody; `not_verified` is a candidate the validator turned down; `duplicate` is a value the duplicate rule withheld; `out_of_date` is an answer over the step's record-age limit; `wrong_line_type` is a number a mobile-only step turned down; `skipped` is a step that never ran \u2014 gated by a run condition, switched off, or reached after an earlier step had already won."
          },
          "reason": {
            "type": "string",
            "nullable": true,
            "description": "One sentence on why — the same sentence the step's own cell shows in the table. `null` on the step that won. For a step that failed it is the provider's error with addresses, keys, tokens and a URL's path and query removed."
          },
          "won": {
            "type": "boolean"
          },
          "value": {
            "type": "string",
            "nullable": true,
            "description": "The answer this step stood behind \u2014 not its raw response. `null` when the step found nothing, never ran, or ran before this was recorded; `outcome` tells those apart."
          },
          "cost": {
            "type": "object",
            "properties": {
              "tokens": {
                "type": "number",
                "description": "The step's whole spend: the provider call plus its validation."
              },
              "usd": {
                "type": "number"
              },
              "providerTokens": {
                "type": "number",
                "description": "The provider call alone. Present only when a validator also ran on this step."
              },
              "validationTokens": {
                "type": "number",
                "description": "The validator call this step paid for. Present only when one ran."
              },
              "unknown": {
                "type": "boolean",
                "description": "The step threw after the provider had been called, so its real spend could not be recovered and `tokens` is a floor rather than a measurement."
              }
            }
          },
          "validation": {
            "type": "object",
            "description": "Present when a validator ran on this step's candidate.",
            "properties": {
              "taskType": {
                "type": "string"
              },
              "verdict": {
                "type": "string"
              },
              "score": {
                "type": "number"
              },
              "accepted": {
                "type": "boolean"
              },
              "strategy": {
                "type": "string"
              },
              "error": {
                "type": "string",
                "description": "Set when the validator itself failed, in which case acceptance fell back to shape alone."
              },
              "reusedFrom": {
                "type": "string",
                "description": "Task type of the earlier step whose identical address the verifier already rejected on this row. Set when the verdict was reused instead of verified again, in which case the step paid nothing for validation."
              }
            }
          },
          "duplicate": {
            "type": "object",
            "description": "Present when the duplicate check ran and the value was not unique.",
            "properties": {
              "count": {
                "type": "integer",
                "description": "Other rows already holding this value when the step ran."
              },
              "threshold": {
                "type": "integer"
              },
              "action": {
                "type": "string",
                "enum": ["flag", "reject"]
              },
              "blocked": {
                "type": "boolean",
                "description": "True when the duplicate rule is what stopped this step from winning."
              }
            }
          },
          "staleness": {
            "type": "object",
            "description": "Present when the step's answer was over its record-age limit.",
            "properties": {
              "days": {
                "type": "integer"
              },
              "maxDays": {
                "type": "integer"
              }
            }
          }
        }
      },
      "IgnoredKeys": {
        "type": "array",
        "items": {
          "type": "string"
        },
        "description": "Keys in the request body that this endpoint does not read, so nothing was applied for them \u2014 most often a misspelt field name. The rest of the write succeeded. Absent when every key was recognised; a body with no recognised keys at all is rejected with 400 instead."
      },
      "CellWriteRejections": {
        "type": "array",
        "description": "Cells in the request that were not written (the first 1,000 at most; the matching `…Count` field has the total), each with the reason. Absent when every cell landed; `updatedCount` counts only what was written. `not_in_table`: the rowId is not a row of this table (never existed here, or deleted). `deduped`: the table's auto-dedupe removed the row the cell would have landed in. `stale`: the stored cell changed since the caller read it (only for writes that opt into a version check), or an append (mode append) another write beat on every retry. `value_too_large`: an append whose combined value grew past the limit under onOversize=reject. `running`: a run is still computing the cell, so it was left as the run has it; send it again once the run finishes.",
        "items": {
          "type": "object",
          "properties": {
            "rowId": { "type": "string" },
            "columnId": { "type": "string" },
            "reason": { "type": "string", "enum": ["not_in_table", "deduped", "stale", "value_too_large", "running"] }
          },
          "required": ["rowId", "columnId", "reason"]
        }
      },
      "SkippedRowIds": {
        "type": "array",
        "description": "Requested row ids that were not deleted (the first 1,000 at most; the matching `…Count` field has the total) because they are not rows of this table. Absent when every id was deleted.",
        "items": {
          "type": "object",
          "properties": { "rowId": { "type": "string" }, "reason": { "type": "string", "enum": ["not_in_table"] } },
          "required": ["rowId", "reason"]
        }
      },
      "SkippedRecords": {
        "type": "array",
        "description": "Records in the request that were not inserted (the first 1,000 at most; the matching `…Count` field has the total), by their zero-based position in the request, each with the reason. `duplicate`: the table's auto-dedupe matched it to an existing row or an earlier record in the same request. Absent when every record was inserted; `insertedCount` counts only what was inserted.",
        "items": {
          "type": "object",
          "properties": { "index": { "type": "integer" }, "reason": { "type": "string", "enum": ["duplicate"] } },
          "required": ["index", "reason"]
        }
      },
      "CappedCells": {
        "type": "array",
        "items": {
          "$ref": "#/components/schemas/CappedCell"
        },
        "description": "Cells stored empty because the submitted value was larger than the column allows. The write still succeeded and every value that fit was stored. Absent when nothing was refused."
      },
      "CappedCell": {
        "type": "object",
        "description": "One cell whose value was refused for size. Values over the limit are stored as empty rather than truncated, because a shortened value is indistinguishable from a complete one.",
        "properties": {
          "rowId": {
            "type": "string",
            "format": "uuid"
          },
          "columnId": {
            "type": "string"
          },
          "originalBytes": {
            "type": "integer",
            "description": "UTF-8 size of the value that was refused."
          },
          "limitBytes": {
            "type": "integer",
            "description": "The ceiling the value was measured against, in bytes. Depends on what the column is for: 8192 for manual columns and for the formula, regex and round-robin tasks, 102400 for reference columns, 204800 for every other AI, API and task column. Trigger columns are exempt."
          }
        }
      },
      "CappedCellDetail": {
        "type": "object",
        "description": "Present on a cell that is empty because the value written to it was larger than the column allows.",
        "properties": {
          "originalBytes": {
            "type": "integer",
            "description": "UTF-8 size of the value that was refused."
          },
          "limitBytes": {
            "type": "integer",
            "description": "The ceiling the value was measured against, in bytes. Depends on what the column is for: 8192 for manual columns and for the formula, regex and round-robin tasks, 102400 for reference columns, 204800 for every other AI, API and task column. Trigger columns are exempt."
          },
          "message": {
            "type": "string",
            "example": "Cell data size exceeds limit (8 kB)",
            "description": "Absent on a cell that recorded no ceiling."
          }
        }
      },
      "Pagination": {
        "type": "object",
        "properties": {
          "nextCursor": {
            "type": "string",
            "nullable": true,
            "description": "On a cursor-paged read: pass it as `cursor` for the next page; `null` when nothing follows. Absent on a `page` read."
          },
          "page": {
            "type": "integer",
            "nullable": true,
            "description": "`null` on a cursor-paged read, which has no page number."
          },
          "pageSize": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "hasMore": {
            "type": "boolean"
          }
        }
      },
      "View": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "emoji": {
            "type": "string"
          },
          "order": {
            "type": "number"
          },
          "filters": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "sorting": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "desc": {
                  "type": "boolean"
                }
              }
            }
          },
          "columnVisibility": {
            "type": "object",
            "additionalProperties": {
              "type": "boolean"
            }
          }
        }
      },
      "ShareLink": {
        "type": "object",
        "description": "A share link: a URL anyone can open to read the whole table, without signing in.",
        "properties": {
          "shareId": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "description": "The link. Whoever holds it can read every row and column of the table until it is revoked or expires."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the link stops working. Null: it works until revoked."
          },
          "mintedBy": {
            "type": "string",
            "enum": ["api_key", "user"],
            "description": "`api_key` for a link made through the API, which belongs to the workspace; `user` for one a person made in the app."
          }
        }
      },
      "Connection": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "provider": { "type": "string", "description": "Provider id, e.g. apollo." },
          "providerName": { "type": "string" },
          "name": { "type": "string", "description": "The connection's name in Loopster." },
          "status": { "type": "string", "description": "connected, pending or error. Only connected can run." },
          "owner": {
            "type": "string",
            "enum": ["workspace", "organization"],
            "description": "workspace for this workspace's own; organization for one a sibling workspace shared, which spends that workspace's provider account."
          },
          "sharedFromWorkspace": {
            "type": "string",
            "description": "The sharing workspace's name. Present only when owner is organization."
          },
          "createdAt": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "Context": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "title": { "type": "string" },
          "type": { "type": "string", "nullable": true, "description": "icp, website, manual or tiers; null when never recorded." },
          "hasContent": {
            "type": "boolean",
            "description": "False when the context has no text yet, so a column reading it gets nothing."
          },
          "updatedAt": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "RetiredSteps": {
        "type": "array",
        "description": "Present only when there are any: steps of the live body whose task Loopster no longer runs. Every run would fail on them, so running the function is refused (409 step_retired) until its author rebuilds them in a sandbox.",
        "items": {
          "type": "object",
          "properties": {
            "header": { "type": "string" },
            "taskType": { "type": "string" },
            "replacedBy": { "type": "string", "description": "The task that took over, where there is one." },
            "message": { "type": "string" }
          }
        }
      },
      "FunctionSummary": {
        "type": "object",
        "properties": {
          "retiredSteps": { "$ref": "#/components/schemas/RetiredSteps" },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "array",
            "description": "What a run must supply, keyed by id in the run request's inputs object.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "header": {
                  "type": "string"
                },
                "dataShape": {
                  "type": "string"
                },
                "required": {
                  "type": "boolean"
                },
                "readsMetadata": {
                  "type": "boolean",
                  "description": "Present, and `true`, when a step of the function reads this input's metadata (a field reference with `source: \"metadata\"`). A run through this API sends values only, so an output behind such a step is skipped and named in the run's `skipped`. Absent on every other input."
                }
              }
            }
          },
          "outputs": {
            "type": "array",
            "description": "The keys a finished run's outputs object carries.",
            "items": {
              "type": "string"
            }
          },
          "costEstimate": {
            "type": "object",
            "description": "What one run can cost for the steps priced up front. Usage-priced steps (AI models, nested functions) are counted in unpricedSteps and bill on top.",
            "properties": {
              "maxUsdPerRun": {
                "type": "number",
                "description": "The most one run can take from the wallet for the steps priced up front, in US dollars: the sell price the wallet is charged, the same figure a trigger estimate quotes. A waterfall step counts at its whole chain. A step on the workspace's own key is not charged by Loopster and adds $0, so a low figure usually means steps run on your own keys."
              },
              "credits": {
                "type": "number",
                "deprecated": true,
                "description": "Deprecated: an internal unit, kept for one release. Read maxUsdPerRun."
              },
              "prefix": {
                "type": "string",
                "enum": ["~"],
                "deprecated": true,
                "description": "Deprecated with credits."
              },
              "unpricedSteps": {
                "type": "integer",
                "description": "Steps priced only by what they use. Not in maxUsdPerRun."
              }
            }
          },
          "hasUnpublishedChanges": {
            "type": "boolean",
            "description": "True when the function's body has changes its author has not published. Callers always run the published contract, so the inputs and outputs above stay accurate \u2014 but the author is working against a newer one, and the usual symptom is an output key they expect that this contract does not declare. Republishing the function in Loopster is what brings the two back together."
          }
        }
      },
      "FunctionContract": {
        "type": "object",
        "description": "A function's contract: what a run must supply and what it returns.",
        "properties": {
          "inputs": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "header": {
                  "type": "string"
                },
                "dataShape": {
                  "type": "string"
                },
                "required": {
                  "type": "boolean"
                },
                "readsMetadata": {
                  "type": "boolean",
                  "description": "Present, and `true`, when a step of the function reads this input's metadata (a field reference with `source: \"metadata\"`). A run through this API sends values only, so an output behind such a step is skipped and named in the run's `skipped`. Absent on every other input."
                }
              }
            }
          },
          "outputs": {
            "type": "array",
            "description": "The keys a finished run's outputs object carries.",
            "items": {
              "type": "string"
            }
          },
          "outputColumns": {
            "type": "array",
            "description": "The column each output key comes from.",
            "items": {
              "type": "object",
              "properties": {
                "columnId": {
                  "type": "string"
                },
                "key": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "FunctionCostEstimate": {
        "type": "object",
        "description": "What one run can cost for the steps priced up front, priced as GET /v1/functions prices a function. Usage-priced steps (AI models, nested functions) are counted in unpricedSteps and bill on top.",
        "properties": {
          "maxUsdPerRun": {
            "type": "number",
            "description": "The most one run can take from the wallet for the steps priced up front, in US dollars: the sell price the wallet is charged, the same figure a trigger estimate quotes. A waterfall step counts at its whole chain. A step on the workspace's own key is not charged by Loopster and adds $0, so a low figure usually means steps run on your own keys."
          },
          "unpricedSteps": { "type": "integer", "description": "Steps priced only by what they use. Not in maxUsdPerRun." }
        }
      },
      "FunctionStepColumn": {
        "description": "A function step as the step endpoints answer with it: the column, and what one run of it costs.",
        "allOf": [
          { "$ref": "#/components/schemas/Column" },
          {
            "type": "object",
            "properties": {
              "costEstimate": {
                "allOf": [{ "$ref": "#/components/schemas/FunctionCostEstimate" }],
                "description": "What one run of this step costs, priced as GET /v1/functions prices the function. A step billed by what it uses (an AI column) has no price before it runs: `unpricedSteps` is 1 and `maxUsdPerRun` 0."
              }
            }
          }
        ]
      },
      "FunctionDetail": {
        "type": "object",
        "description": "A function as the lifecycle endpoints answer with it. Never the id of the table it keeps its steps in.",
        "properties": {
          "retiredSteps": { "$ref": "#/components/schemas/RetiredSteps" },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": ["active", "paused", "stopped"]
          },
          "published": {
            "type": "boolean",
            "description": "Whether callers have a contract to run. False for a draft."
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "agentAccess": {
            "type": "boolean",
            "description": "Whether agents may list and run it over the API and MCP."
          },
          "sandboxOpen": {
            "type": "boolean"
          },
          "hasUnpublishedChanges": {
            "type": "boolean",
            "description": "Whether the draft differs from the contract callers run."
          },
          "contract": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FunctionContract"
              }
            ],
            "description": "What callers run today."
          },
          "costEstimate": {
            "allOf": [{ "$ref": "#/components/schemas/FunctionCostEstimate" }],
            "description": "What one run costs now: the live body's steps, which is what callers run. For every key, and before the function is published or opened to agents too."
          },
          "draft": {
            "type": "object",
            "description": "The body being edited: the sandbox while one is open, the live one otherwise. What a publish would make live. Returned to keys with the editor role or above; absent for a member key.",
            "properties": {
              "costEstimate": {
                "allOf": [{ "$ref": "#/components/schemas/FunctionCostEstimate" }],
                "description": "What one run would cost as drafted: the body being edited, the sandbox while one is open."
              },
              "inputs": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "header": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    },
                    "required": {
                      "type": "boolean"
                    },
                    "readsMetadata": {
                      "type": "boolean",
                      "description": "Present, and `true`, when a step of the function reads this input's metadata (a field reference with `source: \"metadata\"`). A run through this API sends values only, so an output behind such a step is skipped and named in the run's `skipped`. Absent on every other input."
                    }
                  }
                }
              },
              "steps": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "header": {
                      "type": "string"
                    },
                    "kind": {
                      "type": "string",
                      "enum": ["ai", "api", "task"]
                    },
                    "taskType": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              },
              "outputs": {
                "type": "array",
                "description": "Every column the function could return, and whether it does. `columnId` is what PUT /v1/functions/{functionId}/outputs takes.",
                "items": {
                  "type": "object",
                  "properties": {
                    "columnId": {
                      "type": "string"
                    },
                    "header": {
                      "type": "string"
                    },
                    "group": {
                      "type": "string",
                      "description": "The header of the group column it sits under, when it has one."
                    },
                    "selected": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "AgentTrace": {
        "type": "object",
        "description": "One AI column's run: the agent's steps in order, the tools it called, and what they returned. Read off the cell the run already wrote (LOOPSTR-2054).",
        "properties": {
          "kind": {
            "type": "string",
            "enum": ["agent"]
          },
          "columnId": {
            "type": "string"
          },
          "column": {
            "type": "string",
            "description": "The column's header, falling back to its id."
          },
          "status": {
            "type": "string",
            "nullable": true,
            "description": "The cell's status: complete, unsatisfied, skipped or error."
          },
          "result": {
            "type": "string",
            "nullable": true,
            "description": "What the column answered. Null covers empty, never-run and errored alike \u2014 status tells them apart."
          },
          "error": {
            "type": "string",
            "nullable": true,
            "description": "The failure message, when the cell records one."
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "step": {
                  "type": "integer"
                },
                "toolCalls": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "tool": {
                        "type": "string"
                      },
                      "input": {
                        "type": "string",
                        "description": "Truncated. Absent on the answer tool, whose input is the result itself."
                      },
                      "output": {
                        "type": "string",
                        "description": "Truncated. Absent when the tool returned nothing recordable."
                      }
                    }
                  }
                },
                "tokens": {
                  "type": "integer",
                  "description": "Model tokens for this step. Absent when the run did not record them \u2014 which is not the same as zero."
                },
                "elapsedMs": {
                  "type": "integer",
                  "description": "Absent when the run did not record it."
                }
              }
            }
          },
          "limits": {
            "type": "object",
            "description": "The caps this trace was recorded under. Present so a caller can tell a short run from a truncated one without guessing. There is deliberately no truncated flag: a run of exactly maxSteps and a run cut off at maxSteps are indistinguishable in the stored data.",
            "properties": {
              "maxSteps": {
                "type": "integer"
              },
              "snippetChars": {
                "type": "integer"
              }
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Bad request \u2014 the call was malformed, so nothing was looked up. Every table-scoped endpoint answers this when `tableId` is not a UUID, which is deliberately not a `404`: a typo or a truncated id is something an integration can fix on its own, and reporting it as \"not found\" made it indistinguishable from a table someone deleted.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "NoResults": {
        "description": "The search matched nothing, so no table was created",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Unauthorized \u2014 missing or invalid API key. A key past its expiry answers `code: key_expired`, with the date in `error`; create a new key in Settings \u2192 API keys.",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                { "$ref": "#/components/schemas/ErrorResponse" },
                {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": ["key_expired"],
                      "description": "Present when the key has expired."
                    }
                  }
                }
              ]
            }
          }
        }
      },
      "Forbidden": {
        "description": "The credential is valid but does not grant access: the resource belongs to another workspace, the table's API is disabled, or the key's role is below what the operation needs. A role refusal also carries `requiredRole` and `role`. A key created with spending turned off answers `code: spend_not_allowed` wherever the call would start paid work; for a row or cell write, `columns` names the columns the table's Auto-run would have run, and nothing was written.",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                { "$ref": "#/components/schemas/ErrorResponse" },
                {
                  "type": "object",
                  "properties": {
                    "requiredRole": {
                      "type": "string",
                      "enum": ["member", "editor", "admin"],
                      "description": "Present on a role refusal: the role this operation needs."
                    },
                    "role": {
                      "type": "string",
                      "enum": ["member", "editor", "admin", "owner"],
                      "description": "Present on a role refusal: the role the credential holds."
                    },
                    "code": {
                      "type": "string",
                      "enum": ["spend_not_allowed"],
                      "description": "Present when the key may not start paid work."
                    },
                    "columns": {
                      "type": "array",
                      "items": { "type": "string" },
                      "description": "With `spend_not_allowed` on a row or cell write: the columns the write would have run."
                    }
                  }
                }
              ]
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "InternalError": {
        "description": "Internal server error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests: a rate limit was reached, and nothing was done. Retry after the number of seconds in `Retry-After`; `X-RateLimit-Scope` names the window that refused. The limits are the same on every plan. Every authenticated call, MCP included, counts against 600 per minute and 50,000 per rolling 24 hours for the workspace. Calls that can start paid work also count against 60 per minute: triggering rows (not a dry run), updating cells or a row, running a function, creating a table from a starter, and inserting rows or receiving a webhook when the table has autoplay on. That window refuses while the rate-limit store is unreachable, rather than letting spending run unmetered. A search whose exclusions read one of your tables also counts each read against 20 per minute for the workspace (a list is reused for up to two minutes without counting again). A request without a valid key counts against its calling address instead: 600 per minute, and 10 per minute when the key or token does not exist. The inbound webhook (`POST /v1/webhooks/{secret}`) answers its 429 with `Retry-After` only: whoever holds a webhook URL is not shown the workspace's budget. The body's `code` says which wait it is: `rate_limited` for any of the windows above, `search_concurrency` when a search finds the workspace already has 4 searches in flight (wait about 2 seconds), `search_busy` when Loopster Data itself is busy (wait the seconds in `Retry-After`), and `provider_busy` when Create Table's starter found a provider on Loopster's account at its limit, or paused for a few minutes after a burst of its errors (nothing was charged; wait the seconds in `Retry-After`, which comes without `X-RateLimit-*` headers. When the pause came after part of a Google Maps search was paid for, or a source's own retry was refused after its first request reached the provider, the same Idempotency-Key replays the 429, so retry with a new key).",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Scope": {
            "$ref": "#/components/headers/RateLimitScope"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "IdempotencyConflict": {
        "description": "The Idempotency-Key cannot be used for this request. `code` is `idempotency_key_reused` (the key was used for a different request), `idempotency_key_in_progress` (the first request is still running; retry after `Retry-After` seconds) or `idempotency_outcome_unknown` (the first request never finished, so whether it started work is unknown). Nothing was run.",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": ["idempotency_key_reused", "idempotency_key_in_progress", "idempotency_outcome_unknown"]
                    }
                  }
                }
              ]
            }
          }
        }
      },
      "AccountsUnavailable": {
        "description": "The workspace's connections and saved default accounts could not be read, and they decide which account this would run on, so nothing was created or started. Through the API nothing starts on Loopster's account unless the workspace saved it as the provider's default, and a guess could break that. Retry after the `Retry-After` seconds; the same request then runs. A column or capability whose account does not depend on the workspace (Loopster-native, or Loopster's account only) is not refused.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer",
              "example": 5
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": ["accounts_unavailable"]
                    }
                  }
                }
              ]
            }
          }
        }
      },
      "ValueTooLarge": {
        "description": "With `onOversize=reject`: at least one value is over its column's size limit, so nothing in the request was written. `oversize` names each one.",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": ["value_too_large"]
                    },
                    "oversize": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "rowId": {
                            "type": "string",
                            "description": "The row, on PATCH /cells and PATCH /rows/{rowId}."
                          },
                          "index": {
                            "type": "integer",
                            "description": "The record's position in the request, on POST /rows."
                          },
                          "columnId": {
                            "type": "string"
                          },
                          "bytes": {
                            "type": "integer"
                          },
                          "limit": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              ]
            }
          }
        }
      },
      "InsufficientBalance": {
        "description": "Payment required. `code` says why.\n\n**`trial_expired` or `trial_limit`, on any authenticated call except Get Wallet, Get API Key and feedback.** The account's free trial has locked its data: `trial_expired` when the 14 days are over or a paid plan has ended, `trial_limit` when all 1,000 trial enrichments are used. Nothing is read, written or started until a plan is chosen in the app, which lifts the lock; retrying will not. `error` carries the same sentence the app shows. Checked after the rate limit, so a 429 can come first.\n\n**`insufficient_balance` or `workspace_limit`, on calls that start paid work.** The wallet — or this workspace's own spend limit, when that leaves less — cannot cover even the first row or input, so nothing was created, queued or charged. Paid work holds its worst-case cost before it runs, and `spendableUsd` (the same figure `GET /v1/wallet` reports) is less than that hold. `code` says which limit refused it: `insufficient_balance` (top up the wallet) or `workspace_limit` (raise the workspace's spend limit). Either way, you can also ask for less. An `Idempotency-Key` sent with the request is released by the refusal, so the same key retried after a top-up runs.\n\n**`daily_cogs_limit`, on calls that start paid work.** The account has reached its daily spend safety limit, so nothing was created, queued or charged, and `spendableUsd` is 0. Topping up does not lift it; it resets at midnight UTC, and support can raise it.",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": ["insufficient_balance", "workspace_limit", "daily_cogs_limit", "trial_expired", "trial_limit"],
                      "description": "Which limit refused the call. `insufficient_balance`: the wallet cannot cover it; top up or ask for less. `workspace_limit`: this workspace's own spend limit leaves less than the wallet; raise it or ask for less. `daily_cogs_limit`: the account has reached its daily spend safety limit, and `spendableUsd` is 0. Topping up does not lift it; it resets at midnight UTC, and support can raise it. `trial_expired` or `trial_limit`: the free trial, or a paid plan that has ended, has locked the account; choosing a plan in the app lifts it."
                    },
                    "requiredUsd": {
                      "type": "number",
                      "description": "The most the first row or input can cost: what it would have to hold before it runs."
                    },
                    "spendableUsd": {
                      "type": "number",
                      "description": "What the wallet can spend right now, capped by the workspace's spend limit when one is set."
                    }
                  }
                }
              ]
            },
            "example": {
              "success": false,
              "code": "insufficient_balance",
              "error": "This needs up to $0.05 and the wallet can spend $0.01, so nothing was started. Top up the wallet, or ask for less.",
              "requiredUsd": 0.05,
              "spendableUsd": 0.01
            }
          }
        }
      }
    }
  }
}
