{
 "openapi": "3.1.0",
 "info": {
  "title": "GoRunner API",
  "version": "rolling",
  "summary": "Build, publish and run workflows with an API key.",
  "description": "GoRunner is self-hosted workflow automation. This is the API an external client — or an\nLLM holding an API key — drives to author workflows, publish them, run them in production\nand read what happened.\n\n## There is no URL version\n\nOne API, always, that updates in place. There is no `/v1/`, and there never will be. Changes\nare additive: fields are added, never repurposed; a field that must go is deprecated in this\ndocument before it is removed. `info.version` above is `rolling`: this file describes the API as it is now, which is what\nGoRunner Cloud runs. Each self-hosted release stamps its own copy with its version (`v2026.10.25`),\nso no version in it is ever typed by hand.\n\nThe one exception is the embed API, under `/api/embed/v1` (the operations tagged `Embed: …`). It is a\nseparate contract — for a partner's server, which provisions its customers and installs their workflows,\nand for the frame that shows the product inside the partner's own app — versioned with the embed protocol\n(`gorunner-embed/1`). It has its own credentials: an embed API key (`Authorization: Bearer gre_…`) for the\npartner's server; a one-time login code, then an embed session (`gri_…`), for the frame. `x-embed-auth` on\neach of its operations names the one it takes. Every time it answers is RFC 3339 in UTC (`2026-10-16T15:00:33.512081Z`), whatever zone the installation runs in. What follows about API tokens and scopes is not about it.\n\n## Authentication\n\nSend an API token as `Authorization: Bearer grt_…`. A token carries SCOPES, and they confine\nit: a token holding only `run:read` can read runs and cannot read a workflow definition. Each\noperation below names the scope it needs in `x-permission`.\n\nTokens are workspace-scoped and cannot manage organizations, workspaces or other tokens, and\ncannot reach Keel (the in-product AI) at all — no scope covers it.\n\nA token ends with its maker's access: removed from its workspace or organization, or their account\nclosed, it answers 401 from then on. When their workspace role is lowered, a token holding a scope the\nnew role does not ends the same way; one within the new role keeps working.\n\nA token made inside a partner's product (in its embedded view, or by its server) is judged live on\nevery call. When it no longer works it answers 401 `TOKEN_ENDED`, with `details.reason`: `revoked`,\n`expired`, `access_removed` (its maker left the account), `role_changed` (its maker is no longer an\neditor), `account_deleted` or `workspace_deleted`. A token not made to work with the API answers 403\n`TOKEN_SURFACE_OFF` (`details.surface`: `api`), and one whose account has the Tokens switch off\nanswers 403 `FEATURE_OFF` (`details.switch`: `tokens`).\n\n## The envelope\n\nA success with a body is `{\"data\": …}`; a paged list has `pagination` beside `data`. A `204` has no body, an event stream\nis an event stream, and three public addresses answer in their own shape, each said on its operation: the webhook, the\nresume link and `/api/ready`. Every failure is `{\"error\": {\"code\": \"…\", \"message\": \"…\"}}`,\nincluding a mistyped path (404 `NOT_FOUND`) and a wrong method (405 `METHOD_NOT_ALLOWED`, with `Allow` listing the methods the address takes).\nBranch on `error.code`, which is a stable SCREAMING_SNAKE string; `message` is written for a\nhuman and may change.\n\n`503 SERVICE_UNAVAILABLE` with `Retry-After` means the server could not CHECK the request just now — its\ndatabase or Redis did not answer — and did nothing: the token, the webhook address and the resume link are\nfine. Retry after `Retry-After`. Only a token or an address that is really not there answers `401` / `404`.\n\nIn front of every operation: a `POST`, `PUT` or `PATCH` whose `Content-Type` is set to anything but\n`application/json` answers 415 `INVALID_CONTENT_TYPE`; a NUL character (`%00`, `\\u0000`) in the path, the\nquery or a field such as a name answers 400 `VALIDATION_ERROR` (content, such as a definition, a run's\npayload or an import, is kept with the NUL left out); and a body is read up to 5 MB.\n\n## Rate limits and request ids\n\nCalls are counted per minute for each token, 1,200 unless a self-hosted installation sets `API_RATE_LIMIT`, and one address may send at most four times that. An operation's answer carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`. Past the limit: 429 `RATE_LIMITED` with `Retry-After` in seconds. `CONCURRENCY_LIMIT` and `RUNS_WAITING_LIMIT` are also 429 and carry no `Retry-After`: they count runs, not calls. Every answer carries `X-Request-ID`; quote it when reporting a problem.\n\n## Values\n\nA time is an RFC 3339 string. Its fraction has as many digits as it needs, so parse it rather than compare text. This API answers in the installation's time zone, which is UTC on GoRunner Cloud (`2026-10-30T15:43:35.175091Z`); a self-hosted installation in another zone answers with its offset. An id is a UUID, except the ids of apps, actions and triggers, and a step's `name`, which are short names. Names are camelCase, apart from the keys of what a step or a person wrote (`config`, `triggerData`, `outputData`). Some absent values are `null` and some are left out, as each schema says.\n\n## What this file leaves out\n\nThe AI apps' servers (`/mcp/build`, `/mcp/tools`) and their sign-in (`/oauth/*`, `/.well-known/*`) sit at the root of the address and speak MCP and OAuth 2.1, not this API: https://docs.gorunner.io/mcp/oauth. Routes only the dashboard uses (sign-in, settings, the Platform console) are not a contract and can change without notice.\n\n## A paused installation\n\nA self-hosted installation runs only while a valid license is in force. While it is paused for want of one, every operation that takes an API token answers `503` with `{\"error\": {\"code\": \"PAUSED\", \"message\": \"This installation is paused: nothing can run until a license is loaded.\", \"details\": {\"reason\": \"licence\"}}}` — nothing is read, written or run, and nothing is deleted. A public webhook address answers `503` `PAUSED` with `Retry-After: 3600`, as it does for a suspended account. The embed API answers `403` `EMBED_NOT_LICENSED`, as it does for an installation without the license. Once a license is loaded every operation answers again within a second; schedules and poll triggers carry on from their next time, and nothing that was turned away is run later. GoRunner Cloud never pauses.\n\n## A workflow definition is a TREE\n\nNot a list of steps. `trigger` is the root, and each node chains onward through `nextAction`.\nNesting hangs off `branches[].firstAction`, `firstLoopAction` and `onErrorAction`. Three\ndetails cost integrators the most time, so they are stated here as well as on the schema:\n\n- a step's settings go in `config` — `settings` is a WORKFLOW-level key\n- the trigger names its trigger in a field called `actionId`\n- a trigger's `type` is lowercase (`webhook`, `cron`, …) and an action's `type` is UPPERCASE\n  (`PIECE`, `CODE`, `BRANCH`, `LOOP`)\n\n`POST /api/workflows/validate` judges a definition without writing anything, and every\ndiagnostic it returns names the legal values for what it rejected. Call it while building.\n\n## The shortest path from nothing to a production run\n\n1. `GET /api/pieces?view=index` — the catalog, projected to ~10 KB. The unprojected default is\n   over 200 KB; ask for the index unless you need full schemas.\n2. `GET /api/pieces/{id}/triggers/{triggerId}` and `/actions/{actionId}` — the schemas for the\n   apps you picked.\n3. `POST /api/workflows` — create it.\n4. `POST /api/workflows/{id}/publish` — required before any production run. This is what mints\n   the version production executes, and the webhook token if the trigger is a webhook.\n5. `POST /api/workflows/{id}/run?environment=production` — the request BODY is the trigger\n   payload, which is why the environment is a query parameter. Answers 202: accepted, not\n   finished.\n6. `GET /api/runs/{runId}` — poll until the status is terminal.\n\n## Retrying safely\n\nNine write endpoints accept an `Idempotency-Key` header: create, import and duplicate a workflow,\nrotate its webhook address, run it, use a template, create a connection, and run a run again (one\nrun or many). On the embed API every `POST` made with an embed API key takes one, except five whose\nanswer must not be replayed: `/login-codes`, `/accounts/{accountId}/tokens`,\n`/accounts/{accountId}/connections/oauth2/start`, and `/oauth-requests/{requestId}/complete` and\n`/deny`. Each operation that takes the header says so. A retry with the same\nkey replays the original response and carries `Idempotency-Replayed: true` rather than\nperforming the write twice.\n\nOnly a success (2xx) is kept, so the retry of a request that failed runs again. A key belongs to\nits request: the same key with another method, path, query or body is refused with 422\n`VALIDATION_ERROR`, and a retry while the first request is still running waits up to a second,\nthen gets 409 `CONFLICT`. A key is at most 255 characters (400 `VALIDATION_ERROR` past that).\n\nA key's record is kept 24 hours, and one credential (an API token, an embed API key, a\nsession) holds at most 10,000 of them at a time. A NEW key past that is refused with 429\n`RATE_LIMITED` and a `Retry-After` (the seconds until its oldest key expires), and nothing is\ndone; a retry of a key it already holds is answered as always, and the same request without an\n`Idempotency-Key` goes through."
 },
 "servers": [
  {
   "url": "https://api.gorunner.io",
   "description": "GoRunner Cloud."
  },
  {
   "url": "{origin}",
   "description": "A self-hosted installation: your own address.",
   "variables": {
    "origin": {
     "default": "http://localhost:3002",
     "description": "Scheme and host of your installation. All paths below already begin with /api."
    }
   }
  }
 ],
 "security": [
  {
   "bearerAuth": []
  }
 ],
 "tags": [
  {
   "name": "Workflows",
   "description": "Author, read and organize workflows."
  },
  {
   "name": "Lifecycle",
   "description": "Publish, run, version and test a workflow."
  },
  {
   "name": "Runs",
   "description": "Read what happened, and cancel, delete or approve runs."
  },
  {
   "name": "Pieces",
   "description": "The catalog a definition is written in."
  },
  {
   "name": "Connections",
   "description": "Stored credentials a step references by id."
  },
  {
   "name": "Folders",
   "description": "Organize workflows."
  },
  {
   "name": "Templates",
   "description": "Reusable starting points."
  },
  {
   "name": "Service",
   "description": "Is this installation up, and can it do its work. No token."
  },
  {
   "name": "Editor Internals",
   "description": "The operations GoRunner's own builder uses moment to moment — presence heartbeats, the test-event listener, step testing, and step-data lookups. Documented because every route the server serves is documented; an integration script will almost never need them."
  },
  {
   "name": "Embed: sign-in",
   "description": "Login codes and the embedded frame's session: bootstrap, exchange, narrow, sign out."
  },
  {
   "name": "Embed: accounts",
   "description": "The partner's customer accounts and their workspaces: upsert by the partner's id, suspend, resume, delete, restore."
  },
  {
   "name": "Embed: people",
   "description": "The partner's people: memberships with a role per workspace, and the person everywhere."
  },
  {
   "name": "Embed: product",
   "description": "What the partner's customers have in the product: templates to install, the connections the partner manages for them, their workflows, runs and usage."
  },
  {
   "name": "Embed: AI apps",
   "description": "AI app sign-in: the partner's half of connecting ChatGPT, Claude, VS Code, Cursor or Claude Code to a customer account by signing in (the switch `mcpSignIn`, MCP sign-in, and Platform › What customers get › Developers › AI app sign-in address). The account's MCP address is `/mcp/embed/{account org id}/build` (or `/tools`); the app signs in with OAuth, and the installation sends the person to the sign-in address with ONE parameter, `?gr_request={requestId}`. The partner's page MUST: (1) sign its person in first, and only then (2) read `GET /oauth-requests/{requestId}`, which names the account, the app and where its code goes — the account comes from here, never from the link; (3) check the person belongs to that account (and refuse by role if it wants editors only: `deny` with `role`); (4) ask the person to confirm, every time, even when already signed in: “Connect Claude (claude.ai) to Bakery Ltd? You’ll choose what it may do on the next page.”; (5) on Continue call `complete` and answer `303` to its `continueUrl`; on Cancel call `deny` and do the same. Redirect only to a `continueUrl` the installation answered, and never log one: it carries a one-use ticket. Every 4xx after the request was found carries `error.details.continueUrl`; a bare 404 (an unknown or expired request, or an installation without these routes) is answered by the partner's own line, “This connection request has expired. Go back to your AI app and connect again.” The `oauth-requests` routes have their own per-key budget (600 a minute, the `EMBED_OAUTH_REQUEST_RATE_LIMIT` setting), so anonymous sign-in traffic never starves login codes, and the read is not audited. A connection is judged live on every request: a switch turned off pauses it (it works again when turned back on), and it ends when the person leaves the account or a workspace it reaches, drops below the role they signed in with (a lower role in a login code or `PUT member` does this), is deactivated, when the account is deleted, or when the key that signed it in is revoked. A leaked key must be revoked, not rotated: rotation keeps the connections it signed in. `ai-apps` lists and ends connections: one, a person's or an account's, or every one on the installation."
  }
 ],
 "paths": {
  "/api/runs": {
   "get": {
    "operationId": "listRuns",
    "summary": "List runs across every workflow in the workspace",
    "x-permission": "run:read",
    "description": "Returns runs from all workflows in the token's active workspace, newest first.\n\nIMPORTANT — the environment default: a run records the environment it belongs to, and this list DEFAULTS to `environment=production`. Runs the editor started (`test`) are hidden unless you ask for them. If you triggered a run with `POST /api/workflows/{id}/run` without `?environment=production`, that run is a TEST run and will not appear here by default.\n\nFilter values are checked: an unrecognized `status`, `state`, `sort`, `triggerType`, `environment`, `window` or `waiting`, or a `workflowId` that is not a UUID, is refused with 400 `VALIDATION_ERROR` naming the parameter and the values it takes (until 2026-10 such a value was dropped and every run returned). An empty value is no filter.\n\n`q` searches the whole list (not one page): runs whose id starts with it — a whole id pasted from an `X-Run-Id` header, or its first characters — whose workflow's name contains it, or whose error text contains it, ignoring case. `reason` matches the error text alone.\n\nThis response carries a top-level `pagination` object alongside `data`. Step rows are NOT included here; use `GET /api/runs/{id}` for a run's steps and their output.\n\n`state` names a run's state in the six words the Runs page speaks: `queued` (waiting in the run line), `running` (a worker has it, or it waits on time: a Delay, or an answered link resuming), `held` (waiting on a person), `worked`, `failed`, `cancelled`. `queued`, `running` and `held` ignore the range (`window`, `from`, `to`): what is open now is listed whatever its age. Every other state, and the whole list with no `state`, follows it. It cannot be combined with `status`, `waiting` or `taken`, which keep their meaning.\n\nCursor mode: send `cursor` (empty for the first page) to page by start time with `pagination.nextCursor` and no count (`GET /api/runs/counts` has every total). Rows in cursor mode carry no `triggerData`. `sort=duration` cannot be paged by cursor.",
    "parameters": [
     {
      "name": "window",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "24h",
        "7d",
        "30d",
        "90d",
        "all"
       ]
      },
      "description": "Only runs started within the last 24 hours / 7 / 30 / 90 days; `all` (or leaving it out) is no range. Refused together with `from`/`to`. `queued`, `running` and `held` ignore the range (`window`, `from`, `to`): what is open now is listed whatever its age. Every other state, and the whole list with no `state`, follows it."
     },
     {
      "name": "page",
      "in": "query",
      "required": false,
      "description": "1-based page number. Values below 1 (and unparseable values) become 1. A number too large to use is capped, and reads as a page past the end: empty.",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "default": 1
      }
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "description": "Rows per page. Anything outside 1–100 (including unparseable values) silently becomes 20.",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 20
      }
     },
     {
      "name": "status",
      "in": "query",
      "required": false,
      "description": "Exact run status to filter on. `waiting` returns the runs waiting in line for a free slot (each is also `pending` underneath, so `pending` includes them). Any other value is refused with 400 `VALIDATION_ERROR`.",
      "schema": {
       "$ref": "#/components/schemas/RunStatus"
      }
     },
     {
      "name": "environment",
      "in": "query",
      "required": false,
      "description": "Which environment's runs to return. `production` = real trigger traffic (the default). `test` = runs the editor started (canvas Test, listen capture, a retry of a test run, and any `POST /api/workflows/{id}/run` that did not ask for production). `all` = both. Any other value is refused with 400 `VALIDATION_ERROR`.",
      "schema": {
       "type": "string",
       "enum": [
        "production",
        "test",
        "all"
       ],
       "default": "production"
      }
     },
     {
      "name": "triggerType",
      "in": "query",
      "required": false,
      "description": "How the run began. This is the run's OWN trigger vocabulary, not the workflow's: `api` is a run started through `POST /api/workflows/{id}/run`, `subflow` is a run started by a parent flow's Sub-flow step, `manual` is an editor test run. Any other value is refused with 400 `VALIDATION_ERROR`.",
      "schema": {
       "type": "string",
       "enum": [
        "manual",
        "webhook",
        "cron",
        "poll",
        "error",
        "run_finished",
        "event",
        "api",
        "mcp",
        "subflow"
       ]
      }
     },
     {
      "name": "workflowId",
      "in": "query",
      "required": false,
      "description": "Restrict to one workflow. Must be a UUID; anything else is refused with 400.",
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     },
     {
      "name": "tag",
      "in": "query",
      "required": false,
      "description": "Return only runs carrying this tag exactly (tags are attached during execution by the Tags app). Matching is exact, not prefix or substring.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "sort",
      "in": "query",
      "required": false,
      "description": "`oldest` = oldest start first. `duration` = longest `durationMs` first (runs with no duration last). Omitted = newest start first. Any other value is refused with 400 `VALIDATION_ERROR`, and so is `duration` with `cursor`.",
      "schema": {
       "type": "string",
       "enum": [
        "oldest",
        "duration"
       ]
      }
     },
     {
      "name": "waiting",
      "in": "query",
      "required": false,
      "description": "Only `paused` runs, by what they wait for (their `waitingOn`): `person` = a resume link is outstanding (an approval, or a webhook resume someone has to send), whether or not a timeout stands behind it; `time` = it carries on by itself (a Delay, or an approval whose link has just been answered and is resuming). Paged and counted like any other filter. Any other value is refused with 400 `VALIDATION_ERROR`.",
      "schema": {
       "type": "string",
       "enum": [
        "person",
        "time"
       ]
      }
     },
     {
      "name": "taken",
      "in": "query",
      "required": false,
      "description": "`true` leaves out the runs waiting in the run line for a free slot (a `pending` run no worker has taken; `status=waiting` lists exactly those). With `status=pending` it lists the runs a worker is running — the Runs page's Running is that plus `status=running`. Paged and counted like any other filter. Any other value is no filter.",
      "schema": {
       "type": "boolean"
      }
     },
     {
      "name": "q",
      "in": "query",
      "required": false,
      "description": "Search: runs whose id starts with this (a whole run id, or its first characters) or whose workflow's name or error text contains it, ignoring case. At most 200 characters. Combines with every other filter.",
      "schema": {
       "type": "string",
       "maxLength": 200
      }
     },
     {
      "name": "state",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "failed",
        "held",
        "running",
        "queued",
        "worked",
        "cancelled"
       ]
      },
      "description": "One of the six state words (see the description). Refused with `status`, `waiting` or `taken`."
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "format": "date-time"
      },
      "description": "Only runs started at or after this RFC 3339 instant. Refused with `window`."
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "format": "date-time"
      },
      "description": "Only runs started before this RFC 3339 instant. Must be after `from`. Refused with `window`."
     },
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Cursor mode: empty for the first page, then the `pagination.nextCursor` of the page before. A cursor this API did not hand out is refused with 400."
     },
     {
      "name": "reason",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 200
      },
      "description": "Only runs whose error text contains this, ignoring case; wildcards are literal."
     },
     {
      "name": "group",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "A failure group's `key` from `GET /api/runs/failures`: only that group's runs (failed, in the range; of its workflow's failures, the newest 5,000 are read). A key that was not handed out, or that names a workflow of another workspace (or one that is gone), is a 400 `VALIDATION_ERROR`."
     },
     {
      "name": "ids",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Only these runs: up to 100 run ids, separated by commas (the Runs page reads again the rows it draws from pages it read before). The other filters still apply, so send `window=all&environment=all` to read them whatever their age or environment. Another workspace's ids are simply not in the answer; a malformed id or more than 100 is a 400 `VALIDATION_ERROR`."
     }
    ],
    "responses": {
     "200": {
      "description": "A page of runs. Each item omits `steps`; list items additionally carry `workflowName`, `workflowTriggerType`, `stepTotal` and `stepFailed`, and a `paused` item its wait: `waitStep`, `resumeMode`, `resumeAt` and `waitingOn`. With `cursor`, `pagination` is `{limit, nextCursor}` and the rows carry no `triggerData`.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data",
          "pagination"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Run"
           }
          },
          "pagination": {
           "oneOf": [
            {
             "$ref": "#/components/schemas/RunPagination"
            },
            {
             "$ref": "#/components/schemas/RunCursorPagination"
            }
           ]
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "A filter value this list does not know (`status`, `state`, `sort`, `triggerType`, `environment`, `window`, `waiting`); `state` with `status`, `waiting` or `taken`; a `workflowId` that is not a UUID; `ids` with more than 100 ids or one that is not a UUID; `window` together with `from`/`to`, a `from` or `to` that is not RFC 3339, or a `to` not after `from`; a `q` or `reason` over 200 characters; `tag`, `q` or `reason` with a NUL or bytes that are not UTF-8; a `group` or `cursor` this API did not hand out; or `sort=duration` with `cursor`. Error code `VALIDATION_ERROR`; the message names the parameter.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The run list could not be read. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/runs/waiting": {
   "get": {
    "operationId": "listWaitingRuns",
    "summary": "Which workflows have runs waiting in the line, and how many",
    "x-permission": "run:read",
    "description": "Every workflow of the workspace with runs waiting in the run line for a free slot that have not started, most first, and their total: for each, exactly the runs `POST /api/workflows/{id}/runs/cancel-waiting` would cancel now. Test runs count too. The Runs page's “Cancel N waiting” asks this, names each workflow in its question, then cancels one workflow at a time.",
    "responses": {
     "200": {
      "description": "The workflows with runs in line.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "waiting",
            "workflows"
           ],
           "properties": {
            "waiting": {
             "type": "integer",
             "minimum": 0,
             "description": "Runs waiting in the line that have not started, every workflow together."
            },
            "workflows": {
             "type": "array",
             "description": "Each workflow with runs in line, most first.",
             "items": {
              "type": "object",
              "required": [
               "id",
               "name",
               "waiting"
              ],
              "properties": {
               "id": {
                "type": "string",
                "format": "uuid"
               },
               "name": {
                "type": "string"
               },
               "waiting": {
                "type": "integer",
                "minimum": 1
               }
              }
             }
            }
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the count failed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Lifecycle"
    ]
   }
  },
  "/api/runs/{id}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The run id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "getRun",
    "summary": "Get one run with all of its steps",
    "x-permission": "run:read",
    "description": "The only endpoint that returns step-level data.\n\nSTEP OUTPUT LIVES AT `steps[].outputData`, and each step is addressed by `nodeId` — which is the STEP NAME from the workflow definition (`FlowAction.name`, the same identifier expressions use in `{{steps.<name>.field}}`), NOT the human `displayName` and not a canvas coordinate. To read one step's result, find the element of `steps` whose `nodeId` equals the definition step name and read `outputData`. `inputData` is the resolved config that step actually ran with (under an `input.config` object). Large payloads are offloaded to object storage during the run and rehydrated here transparently, so `inputData`/`outputData` are always plain JSON; a payload that cannot be read back is absent, with the step's `dataUnavailable: true`. With `large=defer`, a value over 64 KB is absent and named under `deferred` with its size instead; read it with `step`.\n\nRead path: an IN-FLIGHT run (pending/running/paused) is served from a live cache, not the database, and carries a REDUCED field set — meaningful values for `id`, `status`, `workflowId`, `triggerType`, `triggerData`, `startedAt`, `steps` and, when paused, `waitStep`/`resumeMode`/`resumeAt`. What its run row records is read from there: `startedAt`, `firstStartedAt`, `environment`, `versionId`/`executedVersionId` and their numbers, `retryOfRunId`, `startedFromStep`, `retriedByRunId`, `parentRunId`, `workspaceId` and `orgId`. `tags` is `null` until the worker saves the run, and for a moment after a cancel. `finishedAt` and `durationMs` are absent while it runs, and present as soon as its result is decided, also before it is saved.\n\nPolling: stop when `status` is one of the TERMINAL values `completed`, `failed`, `cancelled`. `pending`, `running` and `paused` are all non-terminal; a `paused` run is waiting on a durable wait (an approval webhook, or a scheduled resume named by `resumeAt`) and can stay paused indefinitely.",
    "responses": {
     "200": {
      "description": "The run, including its steps.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Run"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "`id` is not a UUID. Error code `VALIDATION_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "No such run in this workspace (error code `NOT_FOUND`), or, with `step`, no step of that name in the run (error code `STEP_NOT_FOUND`; the message lists the run's step names).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ],
    "parameters": [
     {
      "name": "step",
      "in": "query",
      "required": false,
      "description": "Return only this step's record: `steps` holds the one step whose `nodeId` (the step NAME) equals it, read whole even with `large=defer`, and only that step's payloads are read from object storage. Use it when the whole run is too large to take, or to read a value a `large=defer` read left out. `trigger` returns the run with no steps and its `triggerData` whole. A name the run has no step for answers 404 `STEP_NOT_FOUND`, naming the steps it has.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "large",
      "in": "query",
      "required": false,
      "description": "`include` (the default): every value, whatever its size. `defer`: every `inputData`, `outputData` and the `triggerData` over 64 KB of JSON is left out and named, with its size, under the step's or the run's `deferred`; the values a run's read keeps are also bounded at 1 MB in all (past it, the largest are left out too). The step a paused run waits on, and the step `step` names, are read whole. Any other value answers 400 `VALIDATION_ERROR`.",
      "schema": {
       "type": "string",
       "enum": [
        "include",
        "defer"
       ]
      }
     }
    ]
   },
   "delete": {
    "operationId": "deleteRun",
    "summary": "Delete one finished run",
    "x-permission": "run:manage",
    "description": "Deletes a TERMINAL run (`completed`, `failed` or `cancelled`) together with its step rows and its offloaded step payloads. Deletion is permanent and there is no undo.\n\nA run that is `pending`, `running` or `paused` is REFUSED with 409 — cancel it first (`POST /api/runs/{id}/cancel`), then delete it, so no executor or resume job is orphaned.",
    "responses": {
     "200": {
      "description": "The run was deleted.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/RunDeleteResult"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "`id` is not a UUID. Error code `VALIDATION_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "No such run in this workspace. Error code `NOT_FOUND`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "The run is still in flight or paused. Error code `CONFLICT`, message \"Cancel the run before deleting it.\"",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The delete failed. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/runs/{id}/stream": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The run id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "streamRun",
    "summary": "Subscribe to a run's live progress (Server-Sent Events)",
    "x-permission": "run:read",
    "description": "A long-lived `text/event-stream` (SSE), not a JSON document. The connection is opened while the run is executing and relays the worker's progress events as they happen. Send `Accept: text/event-stream`; the response sets `Cache-Control: no-cache` and `X-Accel-Buffering: no`.\n\nWhat arrives on the wire, in order:\n1. One NAMED event `connected` whose data is `{\"runId\":\"<uuid>\"}` — sent immediately, and only confirming that the subscription is live.\n2. Zero or more UNNAMED events (SSE default type `message`), each a single JSON object of shape `{\"type\":..., \"runId\":..., \"nodeId\":..., \"status\":..., \"data\":..., \"error\":...}` — see the RunStreamEvent schema for the `type` vocabulary.\n3. Comment lines `: ping` every 25 seconds to keep the connection alive. These are SSE comments, not events; ignore them.\n\nThings a client MUST know:\n* There is NO replay and NO `Last-Event-ID` support. You only receive events published after you connect; anything that happened before is only available from `GET /api/runs/{id}`. Open the stream before or immediately after starting the run, and reconcile with the run detail when you reconnect.\n* The server CLOSES the stream after the run's terminal event (`run_completed`, `run_failed`, `run_cancelled`) — or, when that event was missed, at the next 25-second ping once the run is over. A stream opened on a run that is already over is answered `204 No Content` (no body), which stops a browser's EventSource from reconnecting; read the result from `GET /api/runs/{id}`.\n* A NAMED event `reconnect` (preceded by `retry: 1000`) means the server is going away (a deploy) or the client fell too far behind: reconnect, and reconcile with the run detail.\n* One person or API token may hold 50 run streams on one API server; the next is refused `429 TOO_MANY_STREAMS`.\n* `run_paused` events carry the wait's details in `data` (`waitStep`, `resumeMode`, `resumeAt`), never its resume token: the stream reaches anyone who may read the run.\n* Event data is progress metadata only; step OUTPUT is not streamed. Read it from `GET /api/runs/{id}` (`steps[].outputData`).",
    "responses": {
     "200": {
      "description": "The event stream is open. Body is `text/event-stream`; it ends after the run's terminal event.",
      "content": {
       "text/event-stream": {
        "schema": {
         "type": "string",
         "description": "SSE frames. The first is `event: connected` with `data: {\"runId\":\"...\"}`; subsequent frames are `data: <RunStreamEvent JSON>`; `: ping` comment lines appear every 25s."
        },
        "examples": {
         "stepThenFinish": {
          "summary": "A step completing, then the run finishing",
          "value": "event: connected\ndata: {\"runId\":\"3c0ee072-37f7-4808-91ed-b713dc2fd0a7\"}\n\ndata: {\"type\":\"step_started\",\"runId\":\"3c0ee072-37f7-4808-91ed-b713dc2fd0a7\",\"nodeId\":\"parse_csv\",\"status\":\"running\",\"data\":{\"attempt\":0}}\n\ndata: {\"type\":\"step_completed\",\"runId\":\"3c0ee072-37f7-4808-91ed-b713dc2fd0a7\",\"nodeId\":\"parse_csv\",\"status\":\"completed\",\"data\":{\"attempt\":0,\"duration_ms\":3}}\n\ndata: {\"type\":\"run_completed\",\"runId\":\"3c0ee072-37f7-4808-91ed-b713dc2fd0a7\",\"status\":\"completed\"}\n\n: ping\n\n"
         }
        },
        "x-event-data-schema": {
         "$ref": "#/components/schemas/RunStreamEvent"
        }
       }
      }
     },
     "204": {
      "description": "The run is already over: nothing more will be said about it, so no stream is opened (no body). A browser's EventSource stops reconnecting on a 204; read the run from `GET /api/runs/{id}`."
     },
     "400": {
      "description": "`id` is not a UUID. Error code `VALIDATION_ERROR` (a normal JSON error response, not a stream).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "No such run in this workspace. Error code `NOT_FOUND`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "`TOO_MANY_STREAMS` — this person or token already holds 50 run streams on this API server. `RATE_LIMITED` — the rate limit. `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "The run could not be looked up just now (the database did not answer). Error code `SERVICE_UNAVAILABLE`, with `Retry-After`: nothing is wrong with the run or the token.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/runs/{id}/cancel": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The run id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "cancelRun",
    "summary": "Request cancellation of a run",
    "x-permission": "run:manage",
    "description": "Asks a `pending`, `running` or `paused` run to stop. Takes no request body.\n\nThe answer is always 200 — read the body to learn what happened:\n* `{\"status\":\"cancelling\",\"cancelled\":true}` — cancellation was REQUESTED, not completed. A running step is executing inside a worker process, so the request is a flag the executor picks up; the run unwinds and settles as `cancelled` shortly after. Poll `GET /api/runs/{id}` until `status` is terminal. (A `pending` run is settled immediately even though the response still says `cancelling`.)\n* `{\"status\":\"cancelled\",\"cancelled\":true}` — a `paused` run, which has no executor to unwind, was settled synchronously.\n* `{\"status\":\"<settled status>\",\"cancelled\":false}` — the run was ALREADY terminal; nothing was done and the reported status is the one it already had. This is not an error, so do not retry.\n\nCancellation is signalled through Redis. If Redis is not configured the call is refused with 503.",
    "responses": {
     "200": {
      "description": "The request was accepted, ignored (already terminal), or settled — distinguish via `cancelled` and `status`.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/RunCancelResult"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "`id` is not a UUID. Error code `VALIDATION_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "No such run in this workspace. Error code `NOT_FOUND`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The cancel flag could not be raised, or a paused run could not be settled. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "Redis is not configured on this instance, so cancellation cannot be signalled. Error code `UNAVAILABLE`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/runs/{id}/decision": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The run id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "decideRun",
    "summary": "Approve or reject a held run",
    "x-permission": "run:manage",
    "description": "Answers the approval a `paused` run waits on, as the step's own approve/reject link would, and records who answered on the step (`resumeData.decidedBy`). The run resumes on a worker shortly after; poll `GET /api/runs/{id}`.\n\nDeciding resumes a run, so it needs `run:manage` (an operator and up), as cancelling does. A reader without it is not given the step's approve/reject links in `GET /api/runs/{id}` either: those links decide for whoever holds them.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "decision"
        ],
        "properties": {
         "decision": {
          "type": "string",
          "enum": [
           "approve",
           "reject"
          ],
          "description": "`approved`/`rejected`, and `decline`/`declined` for a rejection, are accepted too, in any case."
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The answer was recorded and the run is resuming.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "runId": {
             "type": "string",
             "format": "uuid"
            },
            "status": {
             "type": "string",
             "enum": [
              "resuming"
             ]
            },
            "waitStep": {
             "type": "string"
            },
            "decision": {
             "type": "string",
             "enum": [
              "approved",
              "rejected"
             ]
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "`id` is not a UUID, the body is not JSON, or `decision` is none of the words it takes. Error code `VALIDATION_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "No such run in this workspace. Error code `NOT_FOUND`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "The run is not waiting for an approval (`NOT_WAITING`), or it was already answered (`ALREADY_ANSWERED`); `ACCOUNT_SUSPENDED` while its account is suspended.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The approval could not be claimed or handed to a worker. Error code `INTERNAL_ERROR` or `ENQUEUE_FAILED`; the run is still held.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/runs/delete": {
   "post": {
    "operationId": "deleteRunsBulk",
    "summary": "Delete many finished runs in one call",
    "x-permission": "run:manage",
    "description": "Deletes every TERMINAL run (`completed`, `failed`, `cancelled`) among the ids posted; ids that name an in-flight or paused run, a run in another workspace, or a run that no longer exists are SKIPPED rather than failing the call. Deletion is permanent.\n\nThis is a POST, not a DELETE, because it carries a body. The path is literally `/api/runs/delete` — it is matched before `/api/runs/{id}`.\n\nThe counts in the response are: `deleted` = rows actually removed, `requested` = how many of the posted ids were parseable UUIDs, `skipped` = `requested - deleted`. Ids that are not valid UUIDs are dropped silently and are NOT counted in `requested`, so posting only malformed ids returns `{\"deleted\":0,\"requested\":0,\"skipped\":0}` with status 200.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/RunBulkDeleteRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The batch was processed. Compare `deleted` with `requested` to see what was skipped.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/RunBulkDeleteResult"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "The body is not JSON of the expected shape (\"Invalid request body.\"), or `ids` is empty or longer than 500 (\"Provide 1–500 run IDs.\"). Error code `VALIDATION_ERROR` in both cases.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The delete failed. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/stats": {
   "get": {
    "operationId": "getStats",
    "summary": "In-flight run counts in your workspace",
    "x-permission": "run:read",
    "description": "The \"is anything of mine backed up?\" glance. Takes no parameters.\n\n`runs` counts the non-terminal runs in the CALLER'S WORKSPACE — `pending` (waiting in line, or taken by a worker and executing), `waiting` (the part of `pending` waiting in line for a free slot, so do not add the two), `running`, `paused`. Terminal runs are never counted; the four keys are always present, zero included.\n\nNothing here describes the rest of the instance. The fleet's own queue depths and its dead-letter queue span every workspace at once, so they belong to the instance's operator, not to the API a workspace's token reaches.",
    "responses": {
     "200": {
      "description": "The workspace's in-flight run counts.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/RunStats"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The run counts could not be read. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/pieces": {
   "get": {
    "operationId": "listPieces",
    "summary": "List the app catalog",
    "x-permission": "",
    "description": "The catalog of apps the worker fleet can currently run — the vocabulary every workflow definition is written in.\n\nSIZE WARNING, READ THIS FIRST. The default response is the FULL catalog with every action and trigger schema inlined: about 413 KB for the 65 apps of the standard build (~100,000 tokens). `?view=index` returns the same apps as one-line entries — about 14 KB (~4,000 tokens), a 30x reduction — carrying `id`, `name`, `description`, `category`, `version`, `versions`, `actionIds`, `triggerIds`, `needsConnection`. That is everything needed to CHOOSE an app. Fetch the index first, then fetch the one operation you actually need from `GET /api/pieces/{id}/actions/{actionId}`. Only fetch the default view if you genuinely need every schema at once.\n\n`?view=` accepts exactly one value, `index`. A non-empty value that is not `index` is refused with 400 VALIDATION_ERROR (a mistyped `?view=indx` used to silently serve the full default). An EMPTY `?view=` and any unrecognized query key are ignored and serve the default full catalog.\n\n`q` and `category` are read ONLY when `view=index`; on the default view they are ignored and the full catalog is returned unchanged.\n\nCACHING. Unfiltered responses carry an `ETag` of the form `\"<fingerprint>-<source>\"` (the index view appends `-index`, so the two views never share a validator) plus `Cache-Control: no-cache`. Send it back as `If-None-Match` to get a 304. FILTERED responses (`q` or `category` supplied) carry NO ETag and can never 304.\n\nPROVENANCE. Every response carries `X-GoRunner-Catalog-Source`. `fleet` means live workers published this and flows can run. `last-known` or `compiled` mean NO worker is checked in: apps can still be browsed and flows edited, but nothing can execute. See `GET /api/pieces/catalog-status`.",
    "parameters": [
     {
      "name": "view",
      "in": "query",
      "required": false,
      "description": "Projection to return. Omit (or send empty) for the full catalog (about 413 KB). `index` returns the one-line-per-app projection (about 14 KB) — use this unless you need schemas. Any other non-empty value is a 400.",
      "schema": {
       "type": "string",
       "enum": [
        "index"
       ]
      },
      "example": "index"
     },
     {
      "name": "q",
      "in": "query",
      "required": false,
      "description": "Free-text filter, applied ONLY when `view=index`. RANKED retrieval over the catalog's capabilities (actions and triggers, including their enum vocabulary), so a natural phrase such as 'send an email' or 'when a form is submitted' is answerable rather than only a literal substring. Results come back in rank order, deduplicated by app, capped at 25. A query the ranker cannot place falls back to the previous case-insensitive substring match over id, name and description, so recall can only increase. An empty result means nothing on this instance matches. Ignored (not an error) on the default view.",
      "schema": {
       "type": "string"
      },
      "example": "http"
     },
     {
      "name": "category",
      "in": "query",
      "required": false,
      "description": "Exact, case-insensitive category match, applied ONLY when `view=index`. Categories in the standard 65-app build: Data (39), Logic (11), Triggers (9), Communication (4), HTTP (2). Ignored on the default view.",
      "schema": {
       "type": "string"
      },
      "example": "Logic"
     },
     {
      "name": "If-None-Match",
      "in": "header",
      "required": false,
      "description": "An ETag previously returned by this endpoint FOR THE SAME VIEW. Matches if the header contains the current ETag as a substring, so a comma-separated list works. A match returns 304 with no body.",
      "schema": {
       "type": "string"
      },
      "example": "\"7abf423b2cf0e9d918f30e4a8878984f-fleet-index\""
     }
    ],
    "responses": {
     "200": {
      "description": "The catalog. Shape depends on `view`: an array of full apps by default, an array of index entries when `view=index`. A partner's customer account limited to some apps is served only those (plus the flow's own branch and loop), and the ETag carries that list, so a browser holding another account's catalog — or the unrestricted one — never revalidates it.",
      "headers": {
       "ETag": {
        "description": "Validator for this body: `\"<catalogFingerprint>-<source>\"`, with `-index` appended for the index view. Absent when `q` or `category` was supplied.",
        "schema": {
         "type": "string"
        }
       },
       "Cache-Control": {
        "description": "Always `no-cache` — the response may be stored but must be revalidated before use.",
        "schema": {
         "type": "string"
        }
       },
       "X-GoRunner-Catalog-Source": {
        "description": "Where this catalog came from. Anything other than `fleet` means no worker is checked in and nothing can be executed.",
        "schema": {
         "type": "string",
         "enum": [
          "fleet",
          "last-known",
          "compiled"
         ]
        }
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "oneOf": [
            {
             "type": "array",
             "description": "Default view: every app with full action/trigger schemas.",
             "items": {
              "$ref": "#/components/schemas/CatalogPiece"
             }
            },
            {
             "type": "array",
             "description": "`view=index`: one line per app.",
             "items": {
              "$ref": "#/components/schemas/CatalogIndexEntry"
             }
            }
           ]
          }
         }
        }
       }
      }
     },
     "304": {
      "description": "The `If-None-Match` value matches the current catalog for this view. No body.",
      "headers": {
       "X-GoRunner-Catalog-Source": {
        "description": "Where the catalog came from.",
        "schema": {
         "type": "string",
         "enum": [
          "fleet",
          "last-known",
          "compiled"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — `?view=` carried a non-empty value other than `index`. The message names the value and the only legal projection.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the catalog could not be rendered to JSON.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. Nothing is judged or served on a guess; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Pieces"
    ]
   }
  },
  "/api/pieces/catalog-status": {
   "get": {
    "operationId": "getCatalogStatus",
    "summary": "Report where the served catalog comes from",
    "x-permission": "",
    "description": "Answers 'can this instance actually RUN anything right now?' in one small response, without downloading the catalog.\n\nThe catalog is published BY THE WORKERS, and the API serves the intersection across every live worker — so an app appears only once every worker can run it. `source: fleet` means live workers are checked in. `last-known` (nothing checked in, serving the last published catalog) and `compiled` (nothing has ever been published) both mean `degraded: true`: apps can be read and flows can be edited, but no run will execute until a worker returns.\n\n`liveCatalogs` greater than 1 means a rollout is in progress and the served catalog is an intersection of two or more worker generations — an app or version may temporarily disappear until the fleet is on one build.\n\n`fingerprint` is the same value that prefixes the `ETag` on `GET /api/pieces`, so a caller can detect a catalog change cheaply by polling this endpoint.",
    "parameters": [],
    "responses": {
     "200": {
      "description": "Current catalog provenance.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/CatalogStatus"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Pieces"
    ]
   }
  },
  "/api/pieces/{id}": {
   "get": {
    "operationId": "getPiece",
    "summary": "Get one app",
    "x-permission": "",
    "description": "One app from the SAME source as the list — an app the fleet cannot run is not fetchable here either.\n\nThe default response is the full app with every action and trigger schema inlined: about 12 KB for `http`, 18 KB for `imap`. `?view=summary` drops every schema and returns what it takes to CHOOSE an operation — each action/trigger's id, name, description, `sideEffects`, how many input fields it declares, and the `discriminators` (gate fields, most far-reaching first) worth supplying as query parameters when you then fetch the operation detail. For `http`, about 12 KB comes down to a small fraction of that.\n\nASYMMETRY WITH THE LIST ENDPOINT: here an unrecognized `?view=` value is NOT an error — it silently serves the full app. Only the exact value `summary` selects the projection.\n\nNo ETag is issued for this endpoint (`Cache-Control: no-cache` and `X-GoRunner-Catalog-Source` are still set), so `If-None-Match` has no effect.\n\nAn app's `actions` describe the DEFAULT version only, and a list that is empty is `null` in the full answer (`[]` in `view=summary`): `triggers` on an app with none, `actions` on one that only triggers. `actionsByVersion` / `triggersByVersion` carry the operation ids each version declares, which is what a step pinned to an older `pieceVersion` must be validated against.",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "description": "App id, exactly as it appears in a workflow step's `pieceId` (e.g. `http`, `webhook`, `code`, `schedule`, `subflow`).",
      "schema": {
       "type": "string"
      },
      "example": "http"
     },
     {
      "name": "view",
      "in": "query",
      "required": false,
      "description": "`summary` returns the schema-free projection. Omitted — or any other value — returns the full app.",
      "schema": {
       "type": "string",
       "enum": [
        "summary"
       ]
      },
      "example": "summary"
     }
    ],
    "responses": {
     "200": {
      "description": "The app — full by default, projected when `view=summary`.",
      "headers": {
       "Cache-Control": {
        "description": "Always `no-cache`.",
        "schema": {
         "type": "string"
        }
       },
       "X-GoRunner-Catalog-Source": {
        "description": "Where the catalog came from.",
        "schema": {
         "type": "string",
         "enum": [
          "fleet",
          "last-known",
          "compiled"
         ]
        }
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "oneOf": [
            {
             "$ref": "#/components/schemas/CatalogPiece"
            },
            {
             "$ref": "#/components/schemas/CatalogPieceSummary"
            }
           ]
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — no app with this id in the catalog the fleet can run (message: `Piece not found.`). An app this workspace may not use answers the same 404.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. Nothing is judged or served on a guess; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Pieces"
    ]
   }
  },
  "/api/pieces/{id}/actions/{actionId}": {
   "get": {
    "operationId": "getPieceAction",
    "summary": "Get one action's schema, narrowed",
    "x-permission": "",
    "description": "ONE action's input/output schema — the unit a caller configuring a step actually needs. Fetching the whole app to configure one step costs about 12 KB on `http` and 18 KB on `imap`, most of it describing operations you are not using.\n\nNARROWING IS ARBITRARY `field=value` PAIRS, NOT A FIXED `?operation=`. Every query key that is not the reserved `view` or `version` is read as a value for the schema's OWN property of that name, and properties whose display conditions that value rules out are dropped. Example: `GET /api/pieces/http/actions/send_request?auth_type=BASIC&body_type=json`. There is deliberately no `?operation=` parameter: only some of the shipped actions gate on a field literally called `operation`; others gate on `mode`, `auth_type`, `algorithm`, `body_type`, `criteria`, `frequency` or `response_type`, and `http.send_request` — the biggest action — has no field called `operation` at all. Get the legal gate fields and their values from `GET /api/pieces/{id}?view=summary` (`discriminators`), or from `unresolved` in this response. A `contains_any` gate accepts a comma-separated list as its value.\n\nABSENT IS NOT FALSE. A property whose gate you did not supply is KEPT, listed in `conditionalFields`, and removed from `required` — because telling a caller to fill a field that may not apply is the failure this projection exists to prevent. Supply more gates to resolve them. Measured on `http.send_request`: `auth_password` is not required unfiltered, and becomes required (with the bearer field gone) under `?auth_type=BASIC`.\n\nREAD THE THREE BOOKKEEPING FIELDS. `filteredBy` echoes only the parameters that ACTUALLY gated something. `ignored` names query keys that gate nothing here — a misspelling (`?bodyType=` for `body_type=`) lands there instead of silently doing nothing. `unresolved` names the gates the surviving fields still depend on: the parameters worth adding to narrow further. `dynamicFields` names properties whose legal values are not in this schema at all and must be fetched from `POST /api/pieces/{id}/actions/{actionId}/options/{propertyName}`.\n\nFiltering is TOP LEVEL only — nested properties and array items are never filtered.\n\nStep configuration built from this schema goes in the step's `config` object (NOT `settings`).",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "description": "App id (the step's `pieceId`).",
      "schema": {
       "type": "string"
      },
      "example": "http"
     },
     {
      "name": "actionId",
      "in": "path",
      "required": true,
      "description": "Action id (the step's `actionId`). A wrong id returns 404 with a message naming every action this app has.",
      "schema": {
       "type": "string"
      },
      "example": "send_request"
     },
     {
      "name": "view",
      "in": "query",
      "required": false,
      "description": "Reserved key. It is NOT interpreted as a gate value and currently selects nothing on this endpoint — sending it changes the response only by keeping it out of `ignored`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "version",
      "in": "query",
      "required": false,
      "description": "Reserved key, never a gate value. The app version whose schema to answer — a step pinned to an older version is drawn from that version's schema (a Code step on `1.0.0` is described differently from one on `2.0.0`). Omitted, the default version a new step gets. A version the fleet does not run answers 404 `NOT_FOUND` naming the versions it does; `pieceVersion` in the answer says which version this is.",
      "schema": {
       "type": "string"
      },
      "example": "1.0.0"
     },
     {
      "name": "gateValues",
      "in": "query",
      "required": false,
      "style": "form",
      "explode": true,
      "description": "FREE-FORM PARAMETER SET — the parameter NAMES are the schema's own property names, and `gateValues` is never sent literally. Send e.g. `?auth_type=BASIC&body_type=json`. Values are always text; booleans (`true`/`false`) and numbers are coerced to the declared property type before comparison. Every key that gates nothing is reported back in `ignored`.",
      "schema": {
       "type": "object",
       "additionalProperties": {
        "type": "string"
       }
      },
      "example": {
       "auth_type": "BASIC",
       "body_type": "json"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The action, narrowed to the fields that apply.",
      "headers": {
       "Cache-Control": {
        "description": "Always `no-cache`.",
        "schema": {
         "type": "string"
        }
       },
       "X-GoRunner-Catalog-Source": {
        "description": "Where the catalog came from.",
        "schema": {
         "type": "string",
         "enum": [
          "fleet",
          "last-known",
          "compiled"
         ]
        }
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/CatalogOperationDetail"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — either the app is not in the catalog (`Piece not found.`) or the app has no such action, in which case the message NAMES the ids it does have: `Piece \"http\" has no action \"nope\". It has: send_request`. An app this workspace may not use answers the same 404.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. Nothing is judged or served on a guess; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Pieces"
    ]
   }
  },
  "/api/pieces/{id}/triggers/{triggerId}": {
   "get": {
    "operationId": "getPieceTrigger",
    "summary": "Get one trigger's schema, narrowed",
    "x-permission": "",
    "description": "The trigger-side twin of `GET /api/pieces/{id}/actions/{actionId}`, with identical narrowing semantics: arbitrary `field=value` query pairs, `filteredBy` / `ignored` / `unresolved` / `conditionalFields` bookkeeping, absent-is-not-false requiredness, top-level filtering only. Example: `GET /api/pieces/schedule/triggers/schedule?frequency=every_day` returns 4 of the trigger's 10 fields.\n\nWhat a trigger detail does NOT carry: the `sideEffects` field is always empty here (it is an action-only classification), and the trigger's `type` (`webhook` / `cron` / `poll` / `error` / `run_finished` / `callable`, always lowercase) is on the app, not on this projection — read it from `GET /api/pieces/{id}`.\n\n`sampleData` here is the trigger's DECLARED example. For triggers whose real payload depends on their configuration (Form, Schedule, Callable) it is generic — use `POST /api/pieces/{id}/triggers/{triggerId}/sample` to get the derived one.\n\nIn a workflow definition the trigger names its trigger id in a field called `actionId` (not `triggerId`), and the trigger's configuration goes in `config`.",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "description": "App id (the trigger's `pieceId`).",
      "schema": {
       "type": "string"
      },
      "example": "schedule"
     },
     {
      "name": "triggerId",
      "in": "path",
      "required": true,
      "description": "Trigger id — the value a definition stores in the trigger's `actionId` field. A wrong id returns 404 naming every trigger this app has.",
      "schema": {
       "type": "string"
      },
      "example": "schedule"
     },
     {
      "name": "view",
      "in": "query",
      "required": false,
      "description": "Reserved key, not interpreted as a gate value.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "version",
      "in": "query",
      "required": false,
      "description": "Reserved key, never a gate value, and it selects nothing here: unlike the action endpoint, a trigger is always answered at the default version, whatever this says (no 404, and it is not listed in `ignored`).",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "gateValues",
      "in": "query",
      "required": false,
      "style": "form",
      "explode": true,
      "description": "FREE-FORM PARAMETER SET — send the trigger schema's own property names, e.g. `?frequency=every_day`. Never send a literal `gateValues` key.",
      "schema": {
       "type": "object",
       "additionalProperties": {
        "type": "string"
       }
      },
      "example": {
       "frequency": "every_day"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The trigger, narrowed to the fields that apply.",
      "headers": {
       "Cache-Control": {
        "description": "Always `no-cache`.",
        "schema": {
         "type": "string"
        }
       },
       "X-GoRunner-Catalog-Source": {
        "description": "Where the catalog came from.",
        "schema": {
         "type": "string",
         "enum": [
          "fleet",
          "last-known",
          "compiled"
         ]
        }
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/CatalogOperationDetail"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — unknown app (`Piece not found.`), or unknown trigger with the legal ids named: `Piece \"schedule\" has no trigger \"nope\". It has: schedule`. An app this workspace may not use answers the same 404.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. Nothing is judged or served on a guess; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Pieces"
    ]
   }
  },
  "/api/pieces/{id}/actions/{actionId}/properties/{propertyName}": {
   "post": {
    "operationId": "resolveDynamicProperties",
    "summary": "Resolve a property's dynamic sub-schema",
    "x-permission": "workflow:read",
    "description": "Asks the app to describe a property whose SHAPE is only known once other values are chosen — the fields returned here are not in the static schema at all.\n\nPOST rather than GET because the request carries the partially-filled step configuration (`values`) and, when the app needs credentials, the `connectionId` to resolve them with. The API never decrypts the credential: the request is handed to a worker, which is why this fails with 503 when no worker is checked in.\n\nMost apps do not implement dynamic properties and answer 400 `NOT_SUPPORTED`. In the standard build the one real case is served by the API itself without touching a worker: `pieceId=subflow`, `propertyName=input`, which returns the Input shape the workflow named in `values.workflow_id` actually expects, pre-filled with that workflow's trigger sample and describing the `{{trigger.*}}` paths its steps read. Pass the target workflow's UUID in `values.workflow_id`; with it absent you get a placeholder description instead.\n\n`actionId` is part of the route and is forwarded to the app, but the `subflow` special case is keyed on the app id and property name alone.\n\nSend `Content-Type: application/json` — a different non-empty content type is refused with 415 `INVALID_CONTENT_TYPE`.\n\nRequires `workflow:read`. Resolving a dynamic value is not a catalog read: it can return tenant data (the subflow app's workflow_id option is this workspace's whole workflow list) and, when the request carries a connectionId, the credential is resolved and decrypted on a worker so the app can call the third party.\n\nThe route is `workflow:read`; a request naming a `connectionId` also needs `workflow:write` and `connection:read`, because it calls the service with that connection's credentials (`403 FORBIDDEN` names the one missing).",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "description": "App id.",
      "schema": {
       "type": "string"
      },
      "example": "subflow"
     },
     {
      "name": "actionId",
      "in": "path",
      "required": true,
      "description": "Action id the property belongs to.",
      "schema": {
       "type": "string"
      },
      "example": "run_workflow"
     },
     {
      "name": "propertyName",
      "in": "path",
      "required": true,
      "description": "The property whose sub-schema is being resolved.",
      "schema": {
       "type": "string"
      },
      "example": "input"
     }
    ],
    "requestBody": {
     "required": true,
     "description": "The step configuration so far, plus the connection to resolve credentials with. Send at least `{}`: no body at all is a 400 `INVALID_BODY`.",
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/PieceDynamicPropertiesRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The resolved properties, keyed by property name. Values are full schema properties, the same shape the static schema uses.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/PieceDynamicPropertiesResponse"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "`INVALID_BODY` (the body was not readable JSON) or `NOT_SUPPORTED` (this app declares no dynamic properties — a client mistake, not an outage).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "The token does not carry `workflow:read` (code `FORBIDDEN`). `PIECE_NOT_ALLOWED` — this workspace may not use the app. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "`NOT_FOUND` — the worker has no such app (message `PIECE_NOT_FOUND`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`RESOLVE_FAILED` (the sub-flow input could not be resolved) or `INTERNAL_ERROR` (the worker answered with something unreadable, or failed for another reason).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`WORKER_UNAVAILABLE` — no worker is checked in, so nothing could run the app code. The flow itself is unaffected. Also `PIECES_UNAVAILABLE` — which apps this workspace may use could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Pieces"
    ]
   }
  },
  "/api/pieces/{id}/actions/{actionId}/options/{propertyName}": {
   "post": {
    "operationId": "resolveDynamicOptions",
    "summary": "Resolve a dropdown property's options",
    "x-permission": "workflow:read",
    "description": "Fetches the legal values for a property the schema marks dynamic — the fields listed in `dynamicFields` on the action/trigger detail, i.e. those whose `propertyType` is `DROPDOWN`, `MULTI_SELECT` or `DYNAMIC`. Their values are NOT in the schema; this endpoint is the only way to learn them.\n\nPOST because the request carries the partially-filled configuration (`values`), the `connectionId` whose credentials the app needs, and an optional `searchQuery` for pickers that filter server-side. Resolution runs on a worker (the API never decrypts credentials), except for the workspace pickers below.\n\nTHERE IS NO TRIGGERS/OPTIONS ROUTE. A dropdown that belongs to a TRIGGER must still be fetched through this actions path; the app id and property name are what select the handler. The two workspace pickers served directly by the API — `errortrigger`/`runfinishedtrigger` with `propertyName=watch_workflow` (returns `All workflows` plus every workflow in the workspace) and `subflow` with `propertyName=workflow_id` (returns the workspace's workflows, callable ones first, each label stating its trigger and whether it is unpublished) — accept any `actionId` in the path.\n\nAn app that declares no dynamic options answers 400 `NOT_SUPPORTED`. A `connectionId` this workspace does not hold reads as no connection, not as an error: the app answers as it does without one.\n\nSend `Content-Type: application/json`.\n\nRequires `workflow:read`. Resolving a dynamic value is not a catalog read: it can return tenant data (the subflow app's workflow_id option is this workspace's whole workflow list) and, when the request carries a connectionId, the credential is resolved and decrypted on a worker so the app can call the third party.\n\nThe route is `workflow:read`; a request naming a `connectionId` also needs `workflow:write` and `connection:read`, because it calls the service with that connection's credentials (`403 FORBIDDEN` names the one missing).",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "description": "App id.",
      "schema": {
       "type": "string"
      },
      "example": "subflow"
     },
     {
      "name": "actionId",
      "in": "path",
      "required": true,
      "description": "Action id the property belongs to. Ignored by the two workspace pickers (`subflow.workflow_id`, `errortrigger`/`runfinishedtrigger` `watch_workflow`), which are keyed on app id and property name.",
      "schema": {
       "type": "string"
      },
      "example": "run_workflow"
     },
     {
      "name": "propertyName",
      "in": "path",
      "required": true,
      "description": "The dropdown property whose options are being fetched.",
      "schema": {
       "type": "string"
      },
      "example": "workflow_id"
     }
    ],
    "requestBody": {
     "required": true,
     "description": "The configuration so far, the connection to use, and an optional search term. Send at least `{}`: no body at all is a 400 `INVALID_BODY` (the two workspace pickers do not read it).",
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/PieceDynamicOptionsRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The options. `value` is what goes into the step's `config`; `label` is for display only.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/PieceDropdownOptions"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "`INVALID_BODY` (unreadable JSON body) or `NOT_SUPPORTED` (this app serves no dynamic options).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "The token does not carry `workflow:read` (code `FORBIDDEN`). `PIECE_NOT_ALLOWED` — this workspace may not use the app. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "`NOT_FOUND` — the worker has no such app (message `PIECE_NOT_FOUND`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`RESOLVE_FAILED` (the workspace's workflows could not be listed) or `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`WORKER_UNAVAILABLE` — no worker is checked in, so the app could not be asked. Also `PIECES_UNAVAILABLE` — which apps this workspace may use could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Pieces"
    ]
   }
  },
  "/api/pieces/{id}/triggers/{triggerId}/sample": {
   "post": {
    "operationId": "getTriggerSample",
    "summary": "Get the sample payload a trigger will deliver",
    "x-permission": "workflow:read",
    "description": "Returns what `{{trigger.*}}` will actually contain for a GIVEN trigger configuration — the shape every expression in the workflow is written against.\n\nWhy this exists rather than reading `sampleData` off the catalog: three internal triggers DERIVE their payload from the author's own configuration (a Form's sample is the fields you defined, a Schedule's is its real next fire time, a Callable's is its declared input), so the frozen literal in the catalog is a lie for them. `derived: true` means the answer reflects the config you sent; `derived: false` means it is the trigger's generic declaration.\n\nThe body is optional, and its one key is `config`: `{\"config\": {…}}`. An absent or empty body means 'no config yet' and the worker answers with the declared fallback rather than failing, and so does a body that is not readable, or that sends the trigger's fields at the top level instead of under `config`: check `derived`. Safe to call for ANY trigger: an app that does not implement derivation falls back to its declared sample instead of erroring.\n\nRuns on a worker, so it is 503 when the fleet is down. A triggerId the app does not have answers 404, naming the ones it does: `Piece \"webhook\" has no trigger \"nope\". It has: catch_webhook.`\n\nSend `Content-Type: application/json` if you send a body.\n\nRequires `workflow:read`. Resolving a dynamic value is not a catalog read: it can return tenant data (the subflow app's workflow_id option is this workspace's whole workflow list) and, when the request carries a connectionId, the credential is resolved and decrypted on a worker so the app can call the third party.",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "description": "App id of the trigger.",
      "schema": {
       "type": "string"
      },
      "example": "webhook"
     },
     {
      "name": "triggerId",
      "in": "path",
      "required": true,
      "description": "Trigger id — the value stored in the definition's trigger `actionId`.",
      "schema": {
       "type": "string"
      },
      "example": "catch_webhook"
     }
    ],
    "requestBody": {
     "required": false,
     "description": "`{\"config\": {…}}`, with the trigger's configuration under `config` exactly as it would sit in the definition's trigger. Omit for the declared fallback.",
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/PieceTriggerSampleRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The sample payload plus its provenance.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/PieceTriggerSampleResponse"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "The token does not carry `workflow:read` (code `FORBIDDEN`). `PIECE_NOT_ALLOWED` — this workspace may not use the app. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "`NOT_FOUND` — the worker has no such app (message `PIECE_NOT_FOUND`), or the app has no such trigger (the message names the ones it has).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the worker failed or answered unreadably.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`WORKER_UNAVAILABLE` — no worker is checked in. Also `PIECES_UNAVAILABLE` — which apps this workspace may use could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Pieces"
    ]
   }
  },
  "/api/pieces/schedule/next-runs": {
   "post": {
    "operationId": "previewScheduleNextRuns",
    "summary": "Preview when a schedule would fire",
    "x-permission": "",
    "description": "Projects a schedule trigger's configuration into the cron it compiles to and the next 5 times it would actually fire. A schedule is the one trigger you cannot test by pressing a button, so this is the only feedback available before production; it is computed by the scheduler's own cron code, so the preview cannot disagree with the runtime.\n\nThe path is literal and static — `schedule` here is the app id, not a path parameter. It always previews the `schedule` app.\n\nAN UNUSABLE SCHEDULE IS STILL A 200. An invalid cron or unknown frequency comes back as `{\"cron\":\"\",\"timezone\":\"\",\"error\":\"unknown frequency: ...\"}`, and an unknown time zone with the cron and the zone as sent beside `error` — a normal answer carried in the body, never an HTTP error. When `error` is present, `nextRuns` is absent, and vice versa. Check for `error` before trusting the result.\n\nThe body is optional, and its one key is `config`: `{\"config\": {\"frequency\": \"every_day\"}}`. An absent or unreadable body, or the trigger's fields sent at the top level instead of under `config`, previews the schedule app's DEFAULTS (hourly, weekdays only) with no error. Note that `run_on_weekends` defaults to false, so `frequency: every_day` compiles to `0 H * * 1-5` and skips Saturday and Sunday — the preview is where you catch that.\n\nThe number of fire times is fixed at 5 and cannot be requested.\n\nRuns on a worker, so it is 503 when the fleet is down. Send `Content-Type: application/json` if you send a body.",
    "parameters": [],
    "requestBody": {
     "required": false,
     "description": "`{\"config\": {…}}`, with the schedule trigger's `config` under `config`, exactly as it would sit in the definition. Get its legal fields from `GET /api/pieces/schedule/triggers/schedule?frequency=<value>`.",
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/ScheduleNextRunsRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The compiled cron plus the next 5 fire times — or, for an unusable schedule, `error` instead of `nextRuns`.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/ScheduleNextRunsResponse"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — the body is not JSON, or `config` is not an object. No body at all previews the default schedule.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — the fleet's catalog has no `schedule` app (message `PIECE_NOT_FOUND`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the worker failed or answered unreadably.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`WORKER_UNAVAILABLE` — no worker is checked in.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Pieces"
    ]
   }
  },
  "/api/workflows/{id}/publish": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID). A non-UUID value is a 400, not a 404.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "publishWorkflow",
    "summary": "Publish the current draft and make the workflow live",
    "x-permission": "workflow:publish",
    "description": "Snapshots the DRAFT (`workflow.definition`) into a new immutable version and sets `status` to `active`. Production traffic executes the SNAPSHOT, never the draft — so editing a live workflow changes nothing until you publish again.\n\nWhat a caller must know:\n- **This is the endpoint that mints the webhook token.** If the workflow's `triggerType` is `webhook` and it has no token yet, publishing generates one and the response carries it as `webhookToken` — to a credential holding `workflow:run`; below that the field is left out, as on `GET /api/workflows/{id}`. That token is the workflow's public production door: `POST /api/webhooks/{webhookToken}`, which is UNAUTHENTICATED — the unguessable URL is the credential. Anyone holding it can start production runs. To revoke it, call `POST /api/workflows/{id}/webhook/rotate`.\n- Publishing an UNCHANGED definition mints no new version (`CreateIfChanged`); it still returns 200 and the same `latestVersionId`. Publish is therefore safe to repeat and carries no `Idempotency-Key`.\n- Publishing is required before `POST /api/workflows/{id}/run?environment=production` will work.\n- Re-publishing an already-active workflow first tears down any self-registered external webhook (Stripe/Telegram-style trigger apps) and re-registers it. That is best-effort and never fails the publish.\n- The body is optional: `{\"expectedRev\": N}`, the `definitionRev` of the draft you hold, makes the publish conditional — a draft that moved since (someone else saved) is 409 and nothing is published, the same rule as a save. An editor that adopts the rev the publish leaves needs it, or it would hide that save and overwrite it with its next one. Without it (or 0 or less) the publish is unconditional.\n- A publish a person makes by clicking one of the assistant's publish cards (the browser sends `X-Assistant-Chat` and `X-Assistant-Click`) makes the whole draft the version that vouches for where its steps' stored secrets may go in runs nobody clicks, so it must name the comparison the card showed: `{\"review\": {\"rev\": N, \"baseVersionId\": \"…\"}}`. A draft that moved since, or a last person-published version other than `baseVersionId`, is 409 `KEEL_PUBLISH_STALE` and nothing is published. Publishes without those headers never need it.\n\nThe publish gate refuses (422) on the FIRST blocking validation diagnostic — the same validator `POST /api/workflows/validate` reports, so what one reports the other refuses. It also refuses an unparseable cron schedule, approval links nothing waits on, an `errorWorkflowId` that cannot fire, and an error/run-finished trigger watching a workflow that cannot be watched. The message is a sentence for a person: the step by its display name, no step key or app id beside it. `error.details` carries the rest: `code` (the diagnostic's), `step` (display name), `stepKey`, `field`, and `alternatives` where there is a closed set of legal values. `POST /api/workflows/validate` keeps the validator's own wording.\n\nPublishing (like pausing) is not an edit: `updatedAt` stays. `definitionRev` does move, on every publish and unpublish, even one that changes nothing: take the new one from the answer before you send `expectedRev` again.",
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/WfPublishRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Published. `status` is `active`, `latestVersionId` names the snapshot production now runs, and `webhookToken` is present for a webhook-triggered workflow when the credential holds `workflow:run`.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WfWorkflow"
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID, or `expectedRev` is not a whole number, or the body is not a JSON object (`details.field` = `expectedRev` for both).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "`FORBIDDEN` — the credential lacks `workflow:publish`. `KEEL_CLICK_REQUIRED` — an assistant write no person clicked (`X-Assistant-Chat` without `X-Assistant-Click`) that does not name, in `X-Assistant-Item: {messageId}/{index}`, a card of the caller's own conversation for this act on this workflow that the server decided `auto` in an untainted proposal; nothing changed. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "`CONFLICT` — `expectedRev` did not match the workflow's current `definitionRev` (someone else saved in between; nothing was published), or another write to this workflow held the per-flow lock longer than the short wait. Reload, or retry. `KEEL_PUBLISH_STALE` — a click on the assistant's publish card named no `review`, or a draft or base version that has moved since the card was shown; nothing was published. Ask the assistant for a new card.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "`VALIDATION_ERROR` — the draft is not publishable. The message is one sentence naming the offending step by its display name; `details` carries `code`, `step`, `stepKey`, `field` and `alternatives` where they apply. `PIECE_NOT_ALLOWED` — the definition holds a step on an app this workspace may not use (a partner's customer account limited by the installation's app list, its own list, or its switches); the message names the first such step. It is named before any other fault, since no other fix makes the workflow publishable. A step whose `connectionId` is not one it can use is refused with `details.code` `connection_not_found` (no such connection in this workflow's workspace: never made, deleted, or another workspace's) or `connection_wrong_piece` (made for another service; a trigger may use its action app's, e.g. `gmailtrigger` a `gmail` connection), `details.stepKey` and `details.field` `connectionId`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the snapshot or the row update failed; nothing went live.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. Nothing is judged or served on a guess; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Lifecycle"
    ]
   }
  },
  "/api/workflows/{id}/publish-review": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "getPublishReview",
    "summary": "What publishing the draft would put live",
    "x-permission": "workflow:read",
    "description": "The review of publishing the current draft, compared with the last version a PERSON published (a version the assistant published by itself is passed over): the change in the assistant's card grammar (`diff`), a sentence for every step that holds or reads a secret, a credential or a connection and where it will send it (`callouts`, names only, never a value), and the outline of what goes live with every config emptied. The same computation as the review on the assistant's publish card; no model is called.\n\nA person's click that then publishes through the assistant's surfaces (`X-Assistant-Chat` and `X-Assistant-Click`) names this review: `POST /api/workflows/{id}/publish` with `{\"review\": {\"rev\": rev, \"baseVersionId\": baseVersionId}}` (`\"\"` for a first publish), and is refused with 409 `KEEL_PUBLISH_STALE` once the draft or that version moved. A read (`workflow:read`): the publish it leads to has its own gate.",
    "responses": {
     "200": {
      "description": "The review.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/PublishReview"
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the draft or a version could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Lifecycle"
    ]
   }
  },
  "/api/workflows/{id}/unpublish": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "unpublishWorkflow",
    "summary": "Take the workflow offline",
    "x-permission": "workflow:publish",
    "description": "Sets `status` to `paused`. A workflow that was never published stays `draft`. Triggers stop firing: the scheduler skips it, and its public webhook URL answers 404 to the outside world.\n\nWhat a caller must know:\n- The webhook token is NOT revoked or changed — it is retained so re-publishing restores the same public URL. Only `POST /api/workflows/{id}/webhook/rotate` revokes it.\n- `latestVersionId` and the version history are untouched; the draft is untouched.\n- Any self-registered external webhook is de-registered with the provider (best-effort — a provider error is logged, never surfaced).\n- Any polling-trigger baseline is reset, so a later re-publish starts from \"now\" rather than replaying everything that happened while paused.\n- Runs already in flight are not cancelled.\n- The body is optional: `{\"expectedRev\": N}` makes it conditional on the draft you hold, as for publish — a draft that moved since is 409 and nothing changes. Without it the call is unconditional. Repeating it on an already-paused workflow is a 200 no-op, which is why it carries no `Idempotency-Key`.",
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/WfRowWriteRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Paused. `status` is `paused`.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WfWorkflow"
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID, or `expectedRev` is not a whole number, or the body is not a JSON object (`details.field` = `expectedRev` for both).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "`FORBIDDEN` — the credential lacks `workflow:publish`. `KEEL_CLICK_REQUIRED` — an assistant write no person clicked (`X-Assistant-Chat` without `X-Assistant-Click`) that does not name, in `X-Assistant-Item: {messageId}/{index}`, a card of the caller's own conversation for this act on this workflow that the server decided `auto` in an untainted proposal; nothing changed. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "`CONFLICT` — `expectedRev` did not match the workflow's current `definitionRev` (someone else saved in between; nothing was changed), or another write to this workflow held the per-flow lock longer than the short wait. Reload, or retry.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the row update failed; the workflow is still live.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Lifecycle"
    ]
   }
  },
  "/api/workflows/{id}/webhook/rotate": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "rotateWorkflowWebhookToken",
    "summary": "Revoke the public webhook URL and issue a new one",
    "x-permission": "workflow:write",
    "description": "**This is the revocation path for the webhook token.** The token is a bearer capability: `POST /api/webhooks/{token}` is UNAUTHENTICATED, so anyone who has the URL can start production runs of this workflow. Once it leaks, rotating is the only way to take it back.\n\nThe old address stops working the instant this returns — anything still sending to it starts receiving 404. Re-point every producer at the new URL before rotating, or accept the gap.\n\nWhat a caller must know:\n- Only a workflow whose `triggerType` is `webhook` has a URL to rotate; anything else is a **409 `CONFLICT`**, not a silent no-op.\n- The new token is returned here as `webhookToken` only to a credential holding `workflow:run`; below that the answer carries `id` alone. The same holds afterwards on `GET /api/workflows/{id}`: below `workflow:run` the field is redacted, and the workflow LIST never carries it at all.\n- The full public URL is the API's public base + `/api/webhooks/` + the token.\n- Rotating does NOT re-register the new URL with a self-registering trigger app; re-publish to do that.\n- No request body is read.",
    "parameters": [
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "description": "Opt-in replay protection, ≤255 characters. A retry carrying the same key and the same method+path+query+body replays the original response with `Idempotency-Replayed: true` instead of rotating a second time (which would strand the URL the first response returned). Same key with a different request is 422; a key whose first request is still in flight is 409. Records live 24h.",
      "schema": {
       "type": "string",
       "maxLength": 255
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Rotated. The previous token is dead.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WfWebhookRotateResult"
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID, or the `Idempotency-Key` header is longer than 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "`CONFLICT` — either this workflow is not webhook-triggered so it has no URL to regenerate, or the per-flow write lock was busy, or an `Idempotency-Key` request is still in flight.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "`VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the rotation failed; the old token is still live.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Lifecycle"
    ]
   }
  },
  "/api/workflows/{id}/run": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "runWorkflow",
    "summary": "Start a run of this workflow",
    "x-permission": "workflow:run",
    "description": "Creates a run and hands it to a worker. **This returns 202 Accepted — the run has been ACCEPTED, not finished.** Nothing has executed when you get the response: the run comes back with `status: \"pending\"` and no `finishedAt`, no `durationMs`, no steps. Take `data.id` — that is the run id — and then either poll `GET /api/runs/{id}` or subscribe to `GET /api/runs/{id}/stream` (SSE) for progress. Step output lands at `steps[].outputData`, keyed by `steps[].nodeId` (the step's stable name), on the run detail.\n\n**The request BODY is the trigger payload.** Whatever JSON object you POST is handed to the flow as its trigger data and reshaped to look like a real delivery from that trigger's kind, so a webhook flow reads it as `{{trigger.body.…}}`, gains `{{trigger.headers}}`/`{{trigger.query}}`/`{{trigger.method}}`, and a schedule flow gets a schedule-shaped payload. The body is optional; an empty/absent body runs with an empty payload. To bypass reshaping and supply trigger data ALREADY in delivery shape (replaying a past run's exact `triggerData`, or a saved test payload), include `\"__shaped\": true` at the top level — that key is stripped and the rest is passed through verbatim.\n\n**That is exactly why `environment` is a QUERY parameter and not a body field.** The body belongs to the caller; a top-level `environment` key in it would collide with the caller's own payload, and a `__shaped` replay carrying that key would both lose the key and silently flip the run's environment.\n\n### environment\n- `production` — executes the PUBLISHED snapshot (`latestVersionId`), records `triggerType: \"api\"` (`\"mcp\"` when an AI app's tool started it), obeys the workflow's `concurrencyLimit`, and appears in `GET /api/runs` by default. **Publishing is required first**: if the workflow is not `active`, or has no published version, the call is **409 `CONFLICT`** with the message *\"Publish this workflow before running it in production.\"*. If the published version exists but cannot be loaded, it is also 409 (*\"…could not be loaded — republish it\"*) rather than silently falling back to the draft.\n- `test` — executes the live DRAFT, records `triggerType: \"manual\"`, ignores the concurrency limit (so the editor stays responsive), and is filtered out of the default `GET /api/runs`.\n- Omitted — resolves to `production` when the workflow is `active` AND has no unpublished changes; otherwise `test`. Passing `fromNode` or `versionId` forces the default to `test`, because both are rehearsals by construction.\n- Any other value is a 400.\n\n### fromNode / versionId / retryOf\n- `fromNode` — a PARTIAL run: execution starts at that step, with upstream values seeded from earlier test data rather than executed. It must name a step on the TOP-LEVEL chain (not inside a branch arm or loop body) — an interior or unknown name is 422. It is **test-only**: combined with `environment=production` it is a 400. The run records it as `startedFromStep`.\n- `versionId` — replays a specific published snapshot instead of the draft (\"retry as it ran\"). The version must belong to THIS workflow or it is 404. Under `environment=production` it must be omitted or equal `latestVersionId`; anything else is 400, because a production run always executes the published version.\n- `retryOf` — records lineage: this run is a retry of that run. The referenced run must belong to THIS workflow or it is 404. It changes nothing about what executes — pass `versionId`, `fromNode` and the original `triggerData` yourself to actually reproduce a run.\n\nThe definition is parsed and every app it names is checked against what the live worker fleet can run BEFORE a run row is created, so an unrunnable flow is 422 and leaves no pending run behind.\n\nPass `?wait=` to receive the finished run in this same call rather than a run id to poll.",
    "parameters": [
     {
      "name": "environment",
      "in": "query",
      "required": false,
      "description": "Which definition executes and how the run is labelled. Omit to let the server choose (production for a clean published workflow, test otherwise). It is a query parameter because the request body is the trigger payload and a body field would collide with it.",
      "schema": {
       "type": "string",
       "enum": [
        "test",
        "production"
       ]
      }
     },
     {
      "name": "fromNode",
      "in": "query",
      "required": false,
      "description": "Start a PARTIAL run at this step instead of the trigger. Must be a top-level step name (`nextAction` chain of the trigger), not a step inside a branch arm or loop body. Test-only — rejected with `environment=production`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "versionId",
      "in": "query",
      "required": false,
      "description": "Execute this published version's snapshot instead of the draft. Must be a version of this workflow. Under `environment=production` it must be omitted or equal the workflow's `latestVersionId`.",
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     },
     {
      "name": "retryOf",
      "in": "query",
      "required": false,
      "description": "Record this run as a retry of that run id (lineage only — it does not copy the original's payload, version or start step). Must be a run of this workflow.",
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "description": "Opt-in replay protection, ≤255 characters. A retry carrying the same key and the same method+path+query+body replays the original 202 (same run id) with `Idempotency-Replayed: true` instead of starting a second run. The QUERY STRING is part of the fingerprint, so the same key with `?environment=production` and `?environment=test` is a different request and is refused with 422 rather than replayed. Records live 24h.",
      "schema": {
       "type": "string",
       "maxLength": 255
      }
     },
     {
      "name": "wait",
      "in": "query",
      "required": false,
      "description": "Hold the connection for up to this many seconds (0-30) and answer with the FINISHED run instead of a 202. Omit it, or send 0, for the historical behavior.\n\n200 means the run reached a terminal state inside the budget — the body is the run with every step's output, so a failure needs no separate shape: read `status` and the failing step. 202 means it is still going, including when it PAUSED on a delay or an approval; poll GET /api/runs/{id}. The run itself is never bounded by this — it keeps executing past the budget.",
      "schema": {
       "type": "integer",
       "minimum": 0,
       "maximum": 30
      }
     }
    ],
    "requestBody": {
     "required": false,
     "description": "The trigger payload. Any JSON object; it is reshaped into the delivery shape of this workflow's trigger kind unless `__shaped` is true.",
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/WfTriggerPayload"
       },
       "examples": {
        "webhookBody": {
         "summary": "An ordinary payload — becomes {{trigger.body}}",
         "value": {
          "orderId": "A-1001",
          "amount": 42.5
         }
        },
        "shapedReplay": {
         "summary": "Already in delivery shape — passed through verbatim",
         "value": {
          "__shaped": true,
          "method": "POST",
          "headers": {
           "content-type": "application/json"
          },
          "query": {},
          "body": {
           "orderId": "A-1001"
          }
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The run reached a terminal state within `?wait=` — the body is the full run record (`Run`, as `GET /api/runs/{id}` reads it), including every step's output.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Run"
          }
         }
        }
       }
      }
     },
     "202": {
      "description": "Accepted, NOT finished. Without `?wait=` (or with 0) the body is the run row just inserted (`WfWorkflowRun`): `status` is `pending`, `id` is the run id to poll or stream, `environment` and `triggerType` reflect what was resolved, and nothing has executed yet. With `?wait=`, a run still going when the budget ran out — paused on a delay or an approval, or waiting in line, included — is the full run record (`Run`, as `GET /api/runs/{id}` reads it) with the steps done so far; only when the run could not be put in the run line is it the inserted row. With `?wait=`, a run not waited for at all — the server already holding as many waiting callers as it may — is the inserted row with `X-Run-Wait: overloaded` and `Location: /api/runs/{id}`.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "anyOf": [
            {
             "$ref": "#/components/schemas/WfWorkflowRun"
            },
            {
             "$ref": "#/components/schemas/Run"
            }
           ]
          }
         },
         "required": [
          "data"
         ]
        }
       }
      },
      "headers": {
       "X-Run-Wait": {
        "description": "`overloaded` (with `?wait=`): not waited for at all. Absent otherwise.",
        "schema": {
         "type": "string",
         "enum": [
          "overloaded"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID; `environment` is not `test` or `production`; `versionId` or `retryOf` is not a UUID; `fromNode` was combined with `environment=production`; `versionId` under production named something other than the published version; the body is not JSON, or is JSON but not an object (wrap an array or scalar as `{\"__shaped\": true, \"body\": …}`); or `wait` is not a whole number from 0 to 30. No run row is created.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "`FORBIDDEN` — the credential lacks `workflow:run`. `ACCOUNT_SUSPENDED` — the workspace's account is suspended: nothing new starts in it until it is resumed, and its data is kept. No run row is created. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace; or `versionId`/`retryOf` does not belong to this workflow.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "`CONFLICT` — a production run was asked for on a workflow that is not published (\"Publish this workflow before running it in production.\"), or whose published version could not be loaded; or an `Idempotency-Key` request is still in flight. `ACCOUNT_DELETED` — the workspace's account was deleted. No run row is created.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "413": {
      "description": "`PAYLOAD_TOO_LARGE` — the body (the trigger payload) is larger than 5 MB. No run row is created.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "`VALIDATION_ERROR` — the definition cannot be parsed, it names an app or action the live worker fleet cannot run, `fromNode` is not a top-level step, or this `Idempotency-Key` was already used with a different request. No run row is created. `PIECE_NOT_ALLOWED` — the definition holds a step on an app this workspace may not use (a partner's customer account limited by the installation's app list, its own list, or its switches); the message names the first such step. No run row is created.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "`CONCURRENCY_LIMIT` — a PRODUCTION run would exceed the workflow's `settings.concurrencyLimit` and the workflow chose `settings.whenFull: refuse`; retry once a run finishes. (By default a run over the limit is accepted and waits in line instead.) `RUNS_WAITING_LIMIT` — the organization already has its cap of runs waiting in line (10,000 unless the installation sets `RUNS_WAITING_PER_ORG`): \"Too many runs are waiting for this organization. Try again later.\"; or this workflow already has its own cap waiting (1,000 unless the installation sets `RUNS_WAITING_PER_WORKFLOW`): \"Too many runs of this workflow are waiting. Try again later.\" Test runs count against both caps. (`RATE_LIMITED` is the separate per-credential API rate limit.)",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. Nothing is judged or served on a guess; try again. Also `STATE_UNAVAILABLE` — the account's state could not be read, so nothing was started; try again. Also `SERVICE_UNAVAILABLE` (`Retry-After: 5`) — the run could not be recorded because the workflow was held for a moment (being deleted, mostly); nothing was started, try again. With `?wait=`, `STORAGE_UNAVAILABLE` — the run finished but could not be read back in time; it is intact, read it with `GET /api/runs/{id}`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Lifecycle"
    ]
   }
  },
  "/api/webhooks/{token}": {
   "post": {
    "operationId": "deliverWebhook",
    "tags": [
     "Lifecycle"
    ],
    "summary": "Deliver to a workflow's webhook",
    "security": [],
    "description": "A published webhook workflow's public door — for a workflow whose trigger is the Webhook trigger, a hosted Form or an app's own webhook trigger, and no other. **It takes no API token**: the unguessable token in the address is the credential — minted by `POST /api/workflows/{id}/publish` (`webhookToken`), revoked and replaced by `POST /api/workflows/{id}/webhook/rotate` — so anyone holding the URL can start production runs. Every delivery runs the PUBLISHED version, as a production run with `triggerType: webhook`.\n\nWhat a caller must know:\n- **Any body, any `Content-Type`**, up to 5 MB. A JSON body becomes `{{trigger.body}}`; a form-urlencoded or multipart body a map of its fields (a repeated field a list; a multipart file under `files`, by its name: `filename`, `size`, `contentType`, not its bytes); anything else, or JSON that does not parse, its text; no body `{}`. The method, the headers (lower-case names, credentials left out) and the query (without `async`) travel beside it as `{{trigger.method}}`, `{{trigger.headers}}` and `{{trigger.query}}`. A NUL in the body or the query is the sender's data, kept with the NUL left out.\n- **Every method.** `GET`, `PUT`, `PATCH` and `DELETE` are delivered exactly as `POST` is, unless the trigger's `methods` lists only some (405 `METHOD_NOT_ALLOWED`, with `Allow`). `HEAD` answers an empty 200 and `OPTIONS` an empty 204 with `Allow`; neither runs anything. A browser's preflight (an `OPTIONS` with `Origin`) is answered by the installation's CORS policy instead, and a page on another origin is not allowed to read the answer. A provider's verification handshake (`validationToken`, `hub.challenge`, `X-Hook-Secret`) is answered without a run too.\n- **It waits for the run, 30 seconds at most.** Finished in time, the answer is the last step's output AS the body: 200, `application/json`, no envelope (`{}` when no step produced any), and the run in the `X-Run-Id` and `X-Run-Status` headers. Still going after 30 seconds — or paused on a delay, an approval or a webhook, or waiting for a free slot (it waits its 30 seconds in line first) — it is 202 with the run's id to poll. When this server already holds as many waiting callers as it may (in all, or for the workflow's organization), it does not wait at all: 202 at once with `X-Run-Wait: overloaded`, `Location: /api/runs/{id}` and `wait: \"overloaded\"` — the run is recorded and queued like any other and runs in order; read its result by its id. The run itself is not cut off: `settings.timeoutSeconds` bounds it.\n- **`?async=true`** answers 202 as soon as the run is queued, and waits for nothing.\n- **A Return Response step** decides the answer instead: its status (200 when it set none), its headers and its body — JSON, text (`raw`) or a redirect (`Location`, 302 unless it chose another). The type sets `Content-Type` (`application/json`, or `text/plain; charset=utf-8` for `raw`), over any the step gave. With \"respond and keep going\" the answer leaves as soon as the step has run and the rest of the run carries on; a later failure does not change it. A header that would act on this address (a cookie, a security policy, framing) fails the step when the run gets there, and so the delivery with 500; validate warns of it (`response_header_refused`), and publish takes it. The answer carries `X-Run-Id` and `X-Run-Status` too, unless the step set those itself.\n- **The run headers** are on every answer about a run: the 200, the synchronous 202, a Return Response answer, the 409, 500 and 504, and the `STORAGE_UNAVAILABLE` 503. `X-Run-Status` can say `pending` on a 202 for a run already going: `GET /api/runs/{id}` is the authority. Not on `?async=true` (its body has `runId`).\n- **Webhook authentication.** When the trigger requires basic, header or HMAC authentication, a request without it is 401 `UNAUTHORIZED` (basic adds `WWW-Authenticate`) and nothing runs. An authentication the trigger cannot enforce as written — an `auth_type` other than `none`, `basic_auth`, `header_auth` or `hmac`, or a secret that is not text, is blank or is a `{{ }}` expression (nothing in the trigger is resolved before a run) — answers 401 to every request; validate and publish refuse it (`webhook_auth_invalid`). No `auth_type` means `none`.\n- **An app's trigger** (Stripe, GitHub, Slack, …) on this address checks the delivery its own way: a signature that fails is 401, a verification challenge is echoed, and an event it does not subscribe to is acknowledged with an empty 200 and runs nothing. A delivery it accepts is queued and acknowledged at once with an empty 200; nothing waits for that run.\n- While its editor listens (`POST /api/workflows/{id}/listen`), an unpublished workflow's address takes one delivery and runs the DRAFT as a test: 200 `{\"data\": {\"message\", \"runId\", \"listening\": true}}`.\n- A Form trigger's address serves its hosted form instead, for a person's browser: `GET` the page, `POST` a submission (form fields, multipart or a JSON object), answered with a page (200, or 400 and 415 with the form again; 429 and 503 with a page saying so). `HEAD` 200, `OPTIONS` 204, any other method a plain-text 405. It is not this contract.\n- **Rate limit:** `WEBHOOK_RATE_LIMIT` requests a minute from one address (default 3000), counted with the other public pages in the same group (resume links, connection sign-in pages, SSO). Past it: 429 `RATE_LIMITED` with `Retry-After`. The answers carry `X-RateLimit-Limit` and `X-RateLimit-Remaining`.\n- **`Idempotency-Key`** (optional, ≤255 characters): a retry with the same key and the same request (method, query and body) starts no second run. It is answered from the run the key's first delivery recorded, with `Idempotency-Replayed: true`: that run's answer, read from its record, if it has finished (a value the record masks stays masked), else 202 with its id. The same key with another request is 422; a retry while the first delivery is still being recorded is 409 with `Retry-After: 1`. Keys are scoped to the workflow and kept 24 hours; a workflow remembers at most 10,000 at once, and past that a new key runs unprotected (never refused). Without the header every delivery is a run, and a sender that retries after a 202 starts a second run.",
    "parameters": [
     {
      "name": "token",
      "in": "path",
      "required": true,
      "description": "The workflow's `webhookToken`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "async",
      "in": "query",
      "required": false,
      "description": "`true`: answer 202 as soon as the run is queued, without waiting for it. Any other value waits.",
      "schema": {
       "type": "string",
       "enum": [
        "true"
       ]
      }
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "description": "Opt-in replay protection for this workflow, ≤255 characters: a retry with the same key and the same method, query and body starts no second run and is answered from the first delivery's run (`Idempotency-Replayed: true`). Kept 24 hours.",
      "schema": {
       "type": "string",
       "maxLength": 255
      }
     }
    ],
    "requestBody": {
     "required": false,
     "description": "The delivery, any content type: it becomes the run's trigger data (`{{trigger.body}}`).",
     "content": {
      "*/*": {
       "schema": {}
      }
     }
    },
    "responses": {
     "200": {
      "description": "The run finished within 30 seconds (`X-Run-Status: completed`). The body is the output of the last step that completed, exactly as that step produced it — no `data` envelope — or `{}`. Also an app trigger's empty acknowledgement, and a `HEAD`.",
      "headers": {
       "X-Run-Id": {
        "$ref": "#/components/headers/RunId"
       },
       "X-Run-Status": {
        "$ref": "#/components/headers/RunStatus"
       },
       "Idempotency-Replayed": {
        "description": "`true` on the answer to a retry of an `Idempotency-Key`, built from the first delivery's run.",
        "schema": {
         "type": "string",
         "enum": [
          "true"
         ]
        }
       }
      },
      "content": {
       "application/json": {
        "schema": {},
        "example": {
         "orderId": "A-1001",
         "accepted": true
        }
       }
      }
     },
     "202": {
      "description": "Accepted, not finished. Synchronous: the run is still going after 30 seconds, paused, or waiting for a free slot — `status` says which, and the run headers are set. `?async=true`: queued, with `runId` and `message` only. Also when the run line could not be reached: the run is recorded (`status: pending`) and runs once it recovers. And at once, without waiting, when the server holds as many waiting callers as it may (`X-Run-Wait: overloaded`, `wait: \"overloaded\"`, `status: pending`): the run is recorded and queued; read it at `Location`. A replay of an `Idempotency-Key` whose run is still going is this 202 too, with `Idempotency-Replayed: true`.",
      "headers": {
       "X-Run-Id": {
        "$ref": "#/components/headers/RunId"
       },
       "X-Run-Status": {
        "$ref": "#/components/headers/RunStatus"
       },
       "X-Run-Wait": {
        "description": "`overloaded`: the server did not wait for this run at all (its waiting callers are at their limit). Absent otherwise.",
        "schema": {
         "type": "string",
         "enum": [
          "overloaded"
         ]
        }
       },
       "Location": {
        "description": "With `X-Run-Wait: overloaded`: where to read the run (`/api/runs/{id}`).",
        "schema": {
         "type": "string"
        }
       },
       "Idempotency-Replayed": {
        "description": "`true` on the answer to a retry of an `Idempotency-Key`.",
        "schema": {
         "type": "string",
         "enum": [
          "true"
         ]
        }
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "runId",
            "message"
           ],
           "properties": {
            "runId": {
             "type": "string",
             "format": "uuid"
            },
            "status": {
             "type": "string",
             "description": "`running`, `paused`, `waiting` or `pending` (`pending` when not waited for: queued). Absent with `?async=true`."
            },
            "message": {
             "type": "string",
             "description": "A sentence for a person: what to do next."
            },
            "wait": {
             "type": "string",
             "enum": [
              "overloaded"
             ],
             "description": "`overloaded`: not waited for, because the server held as many waiting callers as it may."
            },
            "poll": {
             "type": "string",
             "description": "With `wait: overloaded`: where to read the run."
            }
           }
          }
         }
        },
        "example": {
         "data": {
          "runId": "4d3c2b1a-0f9e-4d8c-b7a6-958473625140",
          "status": "running",
          "message": "Workflow is still running. Poll the run by its id, or append ?async=true to always return immediately."
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — a NUL in the address itself, or a body that could not be read, or an `Idempotency-Key` over 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "description": "UNAUTHORIZED — the trigger's own authentication (basic, header or HMAC) or an app's signature check failed. Basic authentication adds `WWW-Authenticate`. Nothing runs. (No API token is read here.)",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "NOT_FOUND (\"Webhook not found.\") — no workflow has this token (never had, or it was rotated), the workflow is not published, its account was deleted, its trigger's app is not one its workspace may use, or its trigger is not one this address is for: only the Webhook trigger, a hosted Form and an app's own webhook trigger answer here — an MCP tool, a schedule or any other trigger does not, even with a token left from an earlier Webhook trigger. All five answer alike, so the address tells a stranger nothing.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "405": {
      "description": "METHOD_NOT_ALLOWED — the trigger's `methods` leaves this method out. `Allow` lists the ones it takes.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "The run was cancelled, while it waited for a slot or while it ran (`X-Run-Status: cancelled`). The run headers are set. Also `CONFLICT` for an `Idempotency-Key` whose first delivery is still being recorded (`Retry-After: 1`) or whose run has since been deleted.",
      "headers": {
       "X-Run-Id": {
        "$ref": "#/components/headers/RunId"
       },
       "X-Run-Status": {
        "$ref": "#/components/headers/RunStatus"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "runId",
            "status",
            "error"
           ],
           "properties": {
            "runId": {
             "type": "string",
             "format": "uuid"
            },
            "status": {
             "type": "string"
            },
            "error": {
             "type": "string",
             "description": "A sentence for a person. Never the step's own error: read the run for that."
            }
           }
          }
         }
        },
        "example": {
         "data": {
          "runId": "4d3c2b1a-0f9e-4d8c-b7a6-958473625140",
          "status": "cancelled",
          "error": "Workflow run was cancelled"
         }
        }
       }
      }
     },
     "413": {
      "description": "PAYLOAD_TOO_LARGE — the body is over 5 MB. Nothing runs.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "VALIDATION_ERROR — this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Nothing runs. `RATE_LIMITED` — this address sent more than `WEBHOOK_RATE_LIMIT` requests this minute; `Retry-After` says when to try again. `CONCURRENCY_LIMIT` — the workflow refuses runs at its runs-at-once limit (`settings.whenFull: refuse`) and is at it. `RUNS_WAITING_LIMIT` — the organization, or this workflow, already has its cap of runs waiting.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The run failed (`X-Run-Status: failed`). The body says so in a sentence and never carries the step's own error: read the run for that. The run headers are set.",
      "headers": {
       "X-Run-Id": {
        "$ref": "#/components/headers/RunId"
       },
       "X-Run-Status": {
        "$ref": "#/components/headers/RunStatus"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "runId",
            "status",
            "error"
           ],
           "properties": {
            "runId": {
             "type": "string",
             "format": "uuid"
            },
            "status": {
             "type": "string"
            },
            "error": {
             "type": "string",
             "description": "A sentence for a person. Never the step's own error: read the run for that."
            }
           }
          }
         }
        },
        "example": {
         "data": {
          "runId": "4d3c2b1a-0f9e-4d8c-b7a6-958473625140",
          "status": "failed",
          "error": "Workflow execution failed"
         }
        }
       }
      }
     },
     "503": {
      "description": "Nothing was decided, so retry. `SERVICE_UNAVAILABLE` (`Retry-After: 5`) — the address could not be looked up just now; it may well be live. `ACCOUNT_SUSPENDED` or, on a paused self-hosted installation, `PAUSED` (`Retry-After: 3600`) — the workflow's account is not taking deliveries; nothing is recorded. `SERVICE_UNAVAILABLE` (`Retry-After: 5`) can also mean the run could not be recorded because its workflow was held for a moment (being deleted, mostly); nothing is recorded or started, and a retry once the delete is through answers 404. `WORKER_UNAVAILABLE` (`Retry-After: 10`) — no worker is running to answer a synchronous delivery; nothing is recorded (`?async=true` is queued instead). `PIECES_UNAVAILABLE` — which apps the workspace may use could not be read. `STORAGE_UNAVAILABLE` — the run finished, but its step output could not be read back from the object store, so this would not be the workflow's real answer; the run is intact (the run headers are set): retry, or read it with `GET /api/runs/{id}`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "504": {
      "description": "The run failed on a time limit — a step's, a request's, a script's or the workflow's `settings.timeoutSeconds` (`X-Run-Status: failed`). The run headers are set.",
      "headers": {
       "X-Run-Id": {
        "$ref": "#/components/headers/RunId"
       },
       "X-Run-Status": {
        "$ref": "#/components/headers/RunStatus"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "runId",
            "status",
            "error"
           ],
           "properties": {
            "runId": {
             "type": "string",
             "format": "uuid"
            },
            "status": {
             "type": "string"
            },
            "error": {
             "type": "string",
             "description": "A sentence for a person. Never the step's own error: read the run for that."
            }
           }
          }
         }
        },
        "example": {
         "data": {
          "runId": "4d3c2b1a-0f9e-4d8c-b7a6-958473625140",
          "status": "failed",
          "error": "Workflow execution timed out"
         }
        }
       }
      }
     },
     "default": {
      "description": "A Return Response step's answer with a status of its own choosing (a redirect included): its status, headers and body, with the run headers.",
      "headers": {
       "X-Run-Id": {
        "$ref": "#/components/headers/RunId"
       },
       "X-Run-Status": {
        "$ref": "#/components/headers/RunStatus"
       }
      }
     }
    }
   }
  },
  "/api/workflows/{id}/runs": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "listWorkflowRuns",
    "summary": "List this workflow's runs, newest first",
    "x-permission": "run:read",
    "description": "Paginated run history for one workflow. Rows are summaries: `steps` is NOT included — fetch `GET /api/runs/{id}` for per-step detail and `outputData`.\n\nUnlike `GET /api/runs`, this endpoint has NO environment filter: it returns BOTH production and test runs interleaved. Use each row's `environment` field to tell them apart.\n\nRows are a reduced record of the run. They carry no `executedVersionId` or `executedVersionNumber`, `startedFromStep`, `parentRunId`, `retriedByRunId`, `claimedAt`, wait or line fields: an absent `executedVersionId` here does NOT mean the draft ran. A run waiting in line reads `pending` here and `waiting` on `GET /api/runs/{id}`. For all of it, read the run, or `GET /api/runs?workflowId=…&environment=all`.\n\nA workflow that is not in this workspace is a 404, as on every workflow route. An empty page is `data: []`.",
    "parameters": [
     {
      "name": "page",
      "in": "query",
      "required": false,
      "description": "1-based page number. Values below 1 and unparseable values read as 1. A number too large to use is capped, and reads as a page past the end: empty.",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "default": 1
      }
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "description": "Rows per page. Anything below 1 or above 100 falls back to 20 (it is NOT clamped to 100 — `limit=500` returns 20).",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 20
      }
     }
    ],
    "responses": {
     "200": {
      "description": "A page of runs.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "description": "The page's runs; `[]` when it has none.",
           "items": {
            "$ref": "#/components/schemas/WfWorkflowRun"
           }
          },
          "pagination": {
           "$ref": "#/components/schemas/WfPagination"
          }
         },
         "required": [
          "data",
          "pagination"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the query failed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Lifecycle"
    ]
   }
  },
  "/api/workflows/{id}/runs/waiting": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "countWaitingRuns",
    "summary": "How many of this workflow's runs are waiting in the line",
    "x-permission": "run:read",
    "description": "The number of this workflow's runs waiting in the run line for a free slot that have not started: exactly the runs `POST /api/workflows/{id}/runs/cancel-waiting` would cancel now. Unpublishing a workflow does not take them out of the line; ask this first to offer to cancel them too.",
    "responses": {
     "200": {
      "description": "The count.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "waiting"
           ],
           "properties": {
            "waiting": {
             "type": "integer",
             "minimum": 0,
             "description": "Runs waiting in the line that have not started."
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the count failed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Lifecycle"
    ]
   }
  },
  "/api/workflows/{id}/runs/cancel-waiting": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "cancelWaitingRuns",
    "summary": "Cancel every run of this workflow waiting in the line",
    "x-permission": "run:manage",
    "description": "Takes every run of this workflow that is waiting in the run line for a free slot out of it, in one transaction, and settles each one `cancelled` with no `durationMs` (it never started, and the time it waited is not how long it took) and the error `Cancelled by user`. Production and editor test runs alike. Takes no request body.\n\nRuns that have started are left alone: a running run, a held run (`paused`, waiting on a person or a timer) and an answered wait that is waiting for a worker to resume it. Each of those has its own `POST /api/runs/{id}/cancel`. A run a worker takes from the line at the same moment is either cancelled here or runs, never both.\n\nThe audit log gets one `run.cancel` row whose target is the workflow and whose metadata holds the `count`; a call that found nothing waiting writes none. `GET /api/workflows/{id}/runs/waiting` says how many there are before you ask.",
    "responses": {
     "200": {
      "description": "How many waiting runs were cancelled (0 when none were waiting).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "cancelled"
           ],
           "properties": {
            "cancelled": {
             "type": "integer",
             "minimum": 0,
             "description": "The runs this call took out of the line and settled `cancelled`."
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the runs could not be cancelled; nothing was changed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Lifecycle"
    ]
   }
  },
  "/api/workflows/{id}/test-step": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "testWorkflowStep",
    "summary": "Execute one step of the draft and return what it produced",
    "x-permission": "workflow:run",
    "description": "Runs a SINGLE step of the workflow's current DRAFT on a worker and returns its output. Nothing is persisted: no run row, no version, no step history — this is a rehearsal of one step, not a run.\n\n**The step really executes.** It calls the third party for real, so testing a step that sends an email sends the email.\n\nWhat a caller must know:\n- **Failure is reported inside a 200.** A step that threw, a step whose app rejected the config, and \"no worker is available to run this right now\" all come back as HTTP 200 with `success: false` and a human-readable `error`. Branch on `success`, never on the status code. A non-2xx here means the REQUEST was refused (bad id, no permission), not that the step failed.\n- `nodeId` is the step's stable NAME as it appears in the definition tree (`FlowAction.name`), not a display name and not an index.\n- `prevOutputs` seeds `{{steps.<name>.…}}` for this test; `triggerData` seeds `{{trigger.…}}`. Both are optional — without them the step resolves those expressions against nothing.\n- Testing the trigger's own name answers `{\"success\": true, \"output\": {\"message\": \"Trigger executed\"}}`: it runs nothing. A name the draft does not hold answers `success: false` with `step not found in workflow`.\n- The call blocks for up to 45 seconds waiting for a worker; it does not carry `Idempotency-Key` (a duplicate costs one wasted worker call and persists nothing).",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/WfTestStepRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The worker answered — OR nothing could run it. Read `success`: `true` carries `output`, `false` carries `error`.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WfTestStepResult"
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID, the body is not valid JSON, or `nodeId` is missing/empty.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "`FORBIDDEN` — the credential lacks `workflow:run`. `PIECE_NOT_ALLOWED` — the step is on an app this workspace may not use; nothing is queued. `ACCOUNT_SUSPENDED` — the workspace's account is suspended: nothing new starts in it until it is resumed, and its data is kept. Nothing is tested — also when the account was suspended while the step waited for a worker. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "`ACCOUNT_DELETED` — the workspace's account was deleted. Nothing is tested.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the worker's answer could not be decoded.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. Nothing is judged or served on a guess; try again. Also `STATE_UNAVAILABLE` — the account's state could not be read, so nothing was started; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Editor Internals"
    ]
   }
  },
  "/api/workflows/{id}/variables": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "getWorkflowVariables",
    "summary": "List the workflow's variables (secret values never returned)",
    "x-permission": "workflow:read",
    "description": "Workflow variables are constants exposed to expressions. They come in two kinds and they live in different places:\n- **Plain** — stored inside the draft definition under `variables`. Returned here with their `value`. Because they are part of the definition, a change to one reaches production only when you publish.\n- **Sensitive** — encrypted on the workflow row, OUTSIDE the definition (so publishing, exporting, duplicating or restoring a version can never carry a secret). Returned here by NAME ONLY: `isSecret: true` and `value: \"\"`. **There is no read path for a secret value, ever.** A sensitive variable takes effect immediately, without a publish — you must be able to rotate a leaked key without shipping a draft.\n\n`secretsAvailable` is false when the server has no encryption key configured; storing a sensitive variable will then be refused, so check it before offering the option.\n\nOrder is sensitive-first, then by name.",
    "responses": {
     "200": {
      "description": "The full variable set.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WfVariablesResponse"
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the encrypted variables could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Lifecycle"
    ]
   },
   "put": {
    "operationId": "replaceWorkflowVariables",
    "summary": "Replace the whole variable set",
    "x-permission": "workflow:write",
    "description": "**Whole-set replacement, not a patch.** Whatever you send IS the complete set: any variable you omit is deleted — plain ones removed from the definition, sensitive ones removed from the encrypted store. Read with GET first, edit the list, send it all back. `\"variables\": []` deletes them all. A body with no list (`{}`, `\"variables\": null`, a misspelt key) is refused with 400 `VALIDATION_ERROR` (`details.field` = `variables`) and changes nothing.\n\nHow to keep a secret you were never given: a client cannot echo back a sensitive value, because GET never returns one. Send that entry with `isSecret: true` and `keep: true` and no value — the stored value is retained. A sensitive entry WITHOUT `keep` is being given a new value, and that value is required.\n\nRules the server enforces (each is a 422, and the whole write is refused — nothing is partially applied):\n- Names are letters, digits and underscores, not starting with a digit, at most 128 characters: they are read as `{{$vars.NAME}}`.\n- No duplicate names — two rows with the same name is refused rather than silently collapsing onto the last.\n- A sensitive value must be at least 6 characters (counted in bytes). A shorter one is REFUSED, not stored unprotected: run data is masked by matching the VALUE, and a very short value would blank out unrelated text everywhere it appeared.\n- A sensitive value is at most 8,192 bytes, and one workflow holds at most 100 sensitive variables.\n- `isSecret: true` with no encryption key configured on the server (`secretsAvailable: false`) is refused.\n\nOn success the response is the same body `GET` would return — so the sensitive values you just wrote are NOT echoed back.\n\nThe plain and the sensitive variables are written in one write: both land, or neither does (a 500 changes nothing).",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/WfPutVariablesRequest"
       },
       "examples": {
        "mixed": {
         "summary": "One plain variable, one rotated secret, one kept secret",
         "value": {
          "variables": [
           {
            "name": "SLACK_CHANNEL",
            "value": "#alerts",
            "isSecret": false
           },
           {
            "name": "STRIPE_KEY",
            "value": "sk_live_new_value_here",
            "isSecret": true
           },
           {
            "name": "LEGACY_TOKEN",
            "isSecret": true,
            "keep": true
           }
          ]
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Replaced. The body is the post-write variable set, with secret values still withheld, and the workflow's new `definitionRev`.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WfVariablesResponse"
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID, or the body is not valid JSON, or it has no `variables` list (`details.field` = `variables`), or `expectedRev` is not a whole number (`details.field` = `expectedRev`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "`CONFLICT` — the workflow was written since the `expectedRev` you sent (another tab, another person, an API token), or the per-flow write lock was held by another write. Read again before retrying.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "`VALIDATION_ERROR` — an invalid or duplicated name; a sensitive entry with no value and no `keep`; `keep: true` for a name with nothing stored; a sensitive value that is too short or too long; too many sensitive variables; sensitive variables requested on a server with no encryption key; or the workflow's own definition can't be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — reading the existing secrets, or the one write of the definition and the sealed secrets, failed. Nothing was changed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Lifecycle"
    ]
   }
  },
  "/api/workflows/{id}/versions": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "listWorkflowVersions",
    "summary": "List published versions, newest first",
    "x-permission": "workflow:read",
    "description": "A version is a snapshot minted by `POST /api/workflows/{id}/publish`. Publishing an unchanged definition mints nothing, so consecutive publishes of identical bytes appear once.\n\n**`definition` is omitted from every row in this list** — it is several KB per row that the panel never reads. Fetch `GET /api/workflows/{id}/versions/{versionId}` for the snapshot itself.\n\n`change` is what changed against the previous version, keyed on step NAME so a reorder does not read as add-plus-remove; its `summary` is the one-line form (\"Added Log it · Edited Fetch customers\"). `changeMessage` is kept for old clients and is empty: nothing writes it.\n\n`onCanvas` marks the AT MOST ONE version whose snapshot equals the current draft — the answer to \"which version is my canvas holding?\". It is resolved across the whole history, not per row, so several byte-identical snapshots still produce exactly one marked row. Which version is LIVE is a different question: that is the workflow's `latestVersionId`.\n\nAn empty page returns `data: []`.",
    "parameters": [
     {
      "name": "page",
      "in": "query",
      "required": false,
      "description": "1-based page number. Values below 1 and unparseable values read as 1. A number too large to use is capped, and reads as a page past the end: empty.",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "default": 1
      }
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "description": "Rows per page. Anything below 1 or above 100 falls back to 20.",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 20
      }
     }
    ],
    "responses": {
     "200": {
      "description": "A page of versions, without their definitions.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/WfWorkflowVersion"
           }
          },
          "pagination": {
           "$ref": "#/components/schemas/WfPagination"
          }
         },
         "required": [
          "data",
          "pagination"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the query failed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Lifecycle"
    ]
   }
  },
  "/api/workflows/{id}/versions/{versionId}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    },
    {
     "name": "versionId",
     "in": "path",
     "required": true,
     "description": "Version id (UUID). It must belong to this workflow — a real version of a DIFFERENT workflow is a 404, not another tenant's data.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "getWorkflowVersion",
    "summary": "Read one published version, including its full snapshot",
    "x-permission": "workflow:read",
    "description": "Returns the version row WITH `definition` — the exact bytes production executed while this version was live. This is the read that pairs with `POST /api/workflows/{id}/run?versionId=…` (replay a run as it ran) and with restore.\n\n`onCanvas` is NOT computed on this single read; it appears only in the list.",
    "responses": {
     "200": {
      "description": "The version and its snapshot.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WfWorkflowVersion"
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — `id` or `versionId` is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace, no such version, or the version belongs to a different workflow.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Lifecycle"
    ]
   }
  },
  "/api/workflows/{id}/versions/{versionId}/restore": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    },
    {
     "name": "versionId",
     "in": "path",
     "required": true,
     "description": "The version whose snapshot becomes the new draft. Must belong to this workflow.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "restoreWorkflowVersion",
    "summary": "Copy a published version's snapshot over the draft",
    "x-permission": "workflow:write",
    "description": "**Restore rewrites the DRAFT and NEVER touches production.** A live workflow keeps running `latestVersionId` until you publish again — so after restoring, the response's `hasUnpublishedChanges` is what tells you whether the draft now differs from what is live. Restoring the version that is already live leaves `hasUnpublishedChanges: false`.\n\nThe restored definition passes the SAME gate as an editor save: the tree is structurally validated, an ACTIVE workflow's apps are checked against the live worker fleet, and blank app-version pins are filled. So an old snapshot naming an app the current fleet cannot run is refused (422) here, rather than quietly breaking production at the next publish.\n\nOptimistic locking: send `{\"expectedRev\": N}` with the workflow's current `definitionRev`. If the draft moved underneath you, the call is 409 and writes nothing — reload and restore again. Omit `expectedRev` (or send 0 or less, which is ignored) for an unconditional restore.\n\nAlso restored/refreshed as a side effect: the workflow's `triggerType` follows the restored definition, a webhook workflow that somehow had no token gets one, and pinned test data is re-stamped for staleness. Test payloads and pinned data are stored outside the definition, so restoring never deletes them.",
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/WfRestoreVersionRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Restored. `definition` is now the snapshot; `hasUnpublishedChanges` says whether that differs from what is live.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WfWorkflow"
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — `id` or `versionId` is not a UUID, or `expectedRev` is not a whole number, or the body is not a JSON object (`details.field` = `expectedRev` for both).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace, or the version does not belong to it.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "`CONFLICT` — `expectedRev` did not match the workflow's current `definitionRev` (someone else saved in between; nothing was written), or the per-flow write lock was busy.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "`VALIDATION_ERROR` — the snapshot fails the shared definition gate (bad tree, unknown key, or an app the live fleet cannot run on an active workflow). `PIECE_NOT_ALLOWED` — the snapshot holds a step on an app this workspace may not use that the current draft does not.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the write failed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. Nothing is judged or served on a guess; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Lifecycle"
    ]
   }
  },
  "/api/workflows/{id}/listen": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "armWorkflowListener",
    "summary": "Arm a one-shot capture of the next real webhook delivery",
    "x-permission": "workflow:run",
    "description": "Arms this workflow to CAPTURE the next delivery to its public webhook URL, for 120 seconds. This is how you get a real provider's payload into the editor without publishing.\n\nWhat a caller must know:\n- **There is no separate test URL.** `testUrl` is the workflow's own webhook path (`/api/webhooks/{webhookToken}`) — the same address a producer is already registered against. It is returned as a PATH, not an absolute URL; prefix it with the API's public base. A workflow with no token yet (a webhook workflow not saved by a `PUT` or published, or one that is not a webhook) answers 200 with `testUrl: \"\"`: nothing can be delivered until a `PUT` makes the token.\n- It is **one-shot**: the first delivery disarms the listener, so a retry or a refresh cannot double-fire. It also expires by itself after `expiresInSeconds`.\n- What happens on that delivery depends on publication state. If the workflow is NOT active, the delivery runs the DRAFT and is recorded as `environment: \"test\"`, `triggerType: \"webhook\"` — honestly both. If the workflow IS active, the delivery is an ordinary production run; arming does not divert it.\n- Watch the outcome on `GET /api/workflows/{id}/listen-stream`, which pushes a `test_event_captured` event carrying `runId` and the captured `triggerData` once the run is accepted, or `test_event_refused` (`code`, `message`, `triggerData`) when the waiting caps refuse it.\n- Arming requires Redis; without it this is 503.\n- No request body is read. Re-arming simply restarts the 120-second window.",
    "responses": {
     "200": {
      "description": "Armed. Send a delivery to `testUrl` within `expiresInSeconds`.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WfListenArmed"
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "`FORBIDDEN` — the credential lacks `workflow:run`. `ACCOUNT_SUSPENDED` — the workspace's account is suspended: nothing new starts in it until it is resumed, and its data is kept. No listener is armed. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "`ACCOUNT_DELETED` — the workspace's account was deleted. No listener is armed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the listener could not be armed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`UNAVAILABLE` — listening requires Redis and this instance has none configured. Also `STATE_UNAVAILABLE` — the account's state could not be read, so nothing was started; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Editor Internals"
    ]
   }
  },
  "/api/workflows/{id}/listen-stream": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "streamWorkflowListener",
    "summary": "Server-sent events for the armed listener",
    "x-permission": "workflow:read",
    "description": "A long-lived `text/event-stream` carrying what the armed listener catches. Open it BEFORE or right after calling `POST /api/workflows/{id}/listen` — it replays nothing, so an event delivered before you connect is missed.\n\nWire format: standard SSE. The first frame is `event: connected` with `{\"runId\":\"webhooktest:<workflow id>\"}` (the channel's name, despite the field name; it is not a run). Subsequent frames are unnamed `data:` lines carrying a JSON object with a `type` field; the one this endpoint exists for is `type: \"test_event_captured\"`, with `runId` (the run that was created) and `triggerData` (the captured payload — method, headers with lower-case names and the credential headers left out, query, body). It is sent once the run has been accepted. When the waiting caps (or the workflow's refuse-when-full limit) refuse the delivery, no run is made and the frame is `type: \"test_event_refused\"` instead, with `code` (`RUNS_WAITING_LIMIT` or `CONCURRENCY_LIMIT`), `message` (the sentence the sender got with its 429) and `triggerData`; the listener is disarmed either way. Keep-alive comment frames arrive periodically.\n\nThis is a stream, not a JSON endpoint: it does not use the `{\"data\": …}` envelope. Errors before the stream opens do.\n\nThe stream carries real deliveries (body, query, headers), so this also needs `run:read`: a token scoped `workflow:read` alone answers `403`.",
    "responses": {
     "200": {
      "description": "The event stream. It stays open until the client disconnects, or until the server, going away, sends `event: reconnect`.",
      "content": {
       "text/event-stream": {
        "schema": {
         "type": "string",
         "description": "SSE frames. Example:\n\nevent: connected\ndata: {\"runId\":\"<workflow-id>\"}\n\ndata: {\"type\":\"test_event_captured\",\"runId\":\"<run-id>\",\"triggerData\":{\"method\":\"POST\",\"headers\":{},\"query\":{},\"body\":{}}}\n"
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`UNAVAILABLE` — realtime requires Redis and this instance has none configured.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Editor Internals"
    ]
   }
  },
  "/api/workflows/{id}/test-context": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "getWorkflowTestContext",
    "summary": "Recent test runs and one run's per-step outputs, for expression mapping",
    "x-permission": "workflow:read",
    "description": "Answers \"what real data can I write expressions against?\" for the whole workflow at once.\n\n`snapshots` lists up to 10 recent MANUAL runs (`triggerType: \"manual\"`, which is what a test run of this workflow records). **Production traffic is deliberately excluded here** — no webhook or cron payload can leak into the builder through this endpoint. For a per-step picker that DOES span production, use `GET /api/workflows/{id}/step-sources`.\n\n`active` carries one of those runs expanded: `nodes` is a map keyed exactly the way expressions resolve — `\"trigger\"` holds the run's trigger data (`{{trigger.…}}`) and every other key is a step's `nodeId` holding that step's output (`{{steps.<nodeId>.…}}`). Only steps that COMPLETED with output appear. Values under secret-looking keys are redacted.\n\n`active` is null when the workflow has no manual runs at all.\n\nEach snapshot carries what a run recorded, so this also needs `run:read`: a token scoped `workflow:read` alone answers `403`, as `GET /api/workflows/{id}/step-sources` does.",
    "parameters": [
     {
      "name": "snapshot",
      "in": "query",
      "required": false,
      "description": "Expand this run id instead of the most recent. Silently ignored (falls back to the newest run) if it is not a UUID or is not one of the runs listed in `snapshots`.",
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     },
     {
      "name": "large",
      "in": "query",
      "required": false,
      "description": "`include` (the default): every output whole. `defer`: an output (or the trigger data) over 64 KB of JSON is left out of `active.nodes` and named, with its size, under `active.deferred`; read it with `GET /api/workflows/{id}/step-sources/{runId}?step=<name>`. Any other value answers 400 `VALIDATION_ERROR`.",
      "schema": {
       "type": "string",
       "enum": [
        "include",
        "defer"
       ]
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The available snapshots and the expanded one.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WfTestContext"
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID, or `large` is neither `include` nor `defer`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the run history could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Editor Internals"
    ]
   }
  },
  "/api/workflows/{id}/step-sources": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "listWorkflowStepDataSources",
    "summary": "Which past runs hold real data for ONE step",
    "x-permission": "workflow:read",
    "description": "Lists up to 10 recent runs that actually produced data for the named step, newest first. Existence is proven per run, so a run that never reached the step — an upstream failure, a branch not taken, a version where the step did not exist — is simply not offered.\n\n**Unlike `GET /api/workflows/{id}/test-context`, this spans BOTH environments**, because a webhook workflow may only ever have fired in production and refusing to show that run would leave the picker empty for exactly the workflows that need it most. Every row carries its `environment` and `triggerType` so you can label it.\n\n`step` is the step's stable NAME (`FlowAction.name`). Two spellings resolve to the trigger: the literal `\"trigger\"` and the trigger's own configured name — a renamed trigger still stores its payload under the engine's `trigger` key, so both work.\n\nFeed a chosen `runId` back into `GET /api/workflows/{id}/step-sources/{runId}?step=…` to fetch the data itself.",
    "parameters": [
     {
      "name": "step",
      "in": "query",
      "required": true,
      "description": "The step's stable name, or `trigger` (or the trigger's own name) for the trigger payload. Required — an empty or missing value is a 400.",
      "schema": {
       "type": "string",
       "minLength": 1
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The runs that can supply data for this step.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WfStepDataSourceList"
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — the path id is not a UUID, or `step` is missing/blank.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "`FORBIDDEN` — the credential lacks `workflow:read`, or lacks `run:read`, which reading a past run's step data also needs (\"Reading a past run's step data requires run:read.\"). A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "`NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the run history could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Editor Internals"
    ]
   }
  },
  "/api/workflows/{id}/step-sources/{runId}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID).",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    },
    {
     "name": "runId",
     "in": "path",
     "required": true,
     "description": "The run to read from. Ownership is enforced inside the query by workspace AND workflow, so a run id belonging to another workflow or tenant resolves to nothing (404) rather than to data.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "getWorkflowStepDataFromRun",
    "summary": "One step's recorded output from one past run",
    "x-permission": "workflow:read",
    "description": "Returns the output the named step produced in that run — or the run's trigger payload when `step` addresses the trigger. Large payloads that were offloaded to object storage are rehydrated transparently.\n\n**\"No data\" is a 404, not an empty object.** A caller must be able to tell \"this run has nothing for that step\" apart from \"this step genuinely returned `{}`\", so the empty case is refused rather than answered with a value that would silently mislead a mapping tree.\n\nValues under secret-looking keys are redacted before the payload leaves the server.\n\nNote the shape: the payload sits at `data.data` — the outer `data` is the success envelope, the inner `data` is this endpoint's own field name.",
    "parameters": [
     {
      "name": "step",
      "in": "query",
      "required": true,
      "description": "The step's stable name, or `trigger` (or the trigger's own name). Required.",
      "schema": {
       "type": "string",
       "minLength": 1
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The step's recorded output.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WfStepDataFromRun"
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — `id` or `runId` is not a UUID, or `step` is missing/blank.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "`FORBIDDEN` — the credential lacks `workflow:read`, or lacks `run:read`, which reading a past run's step data also needs (\"Reading a past run's step data requires run:read.\"). A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "`NOT_FOUND` — that run has no recorded data for this step (which also covers a run that does not exist or does not belong to this workflow/workspace).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — the step data could not be loaded.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`STORAGE_UNAVAILABLE` — that run recorded data for this step, but it could not be read back from object storage right now. Retry; it is not a missing record.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Editor Internals"
    ]
   }
  },
  "/api/workflows/{id}/presence": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id (UUID). A malformed id answers 404 here, not 400 — presence never discloses whether a flow exists.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "beatWorkflowPresence",
    "summary": "Heartbeat presence and maintain the advisory editing lock",
    "x-permission": "workflow:read",
    "description": "One poll that does three things: records that you have this workflow open, maintains or acquires the ADVISORY editing lock, and carries the take-over handshake — all in a single round trip.\n\n**The lock is advisory.** It does not refuse writes; the real guards are `expectedRev` (optimistic locking) and the per-flow write lock the server takes around every mutation. Its purpose is to let a client show \"someone else is editing\" and go read-only.\n\nPresence evaporates on its own: stop beating and you disappear within about 30 seconds, so a closed tab needs no cleanup. Beat at `beatEverySeconds` from the response — the server owns the interval, do not hard-code one.\n\n**`mine` is computed server-side.** Never compare user ids yourself: sessions and machine principals make that comparison wrong.\n\nThe `takeover` handshake:\n- `\"request\"` — ask the current holder to hand over. The holder sees it in their next beat's `takeover`. If they do not respond, the requester can claim the lock after a ~30-second grace, so an away holder can never strand a workflow.\n- `\"grant\"` — sent by the HOLDER to hand the lock over now. There is deliberately no \"decline\": the service has none, and offering one would be a lie.\n- `\"force\"` — the admin override. It is checked HERE against the caller's `workspace:manage` permission, never trusted from the client; without it the call is 403 (note: this 403 is a permission check on the `takeover` value, not on `workflow:read`).\n- `\"\"` or omitted — an ordinary beat.\n\n`editing: true` means \"I am an editor surface\" and asks for the lock when it is free; it needs `workflow:write`, as `takeover: \"request\"` does. `editing: false` is a passive viewer that only registers presence. An unreadable body, or a `takeover` it does not know, is an ordinary beat, never a 400.",
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/WfPresenceBeatRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Everyone present, who holds the lock, whether it is yours, any pending take-over, and how often to beat.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WfPresenceState"
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — `tab` is longer than 64 characters (`details.field` = `tab`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "`FORBIDDEN` — `editing: true` or `takeover: \"request\"` without `workflow:write`, or `takeover: \"force\"` by a caller without `workspace:manage` against someone else's lock. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "`NOT_FOUND` — the id is not a UUID, or no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "`INTERNAL_ERROR` — presence is unavailable (Redis). Treat the flow as unlocked: presence fails OPEN by design and must never act as a jailer.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Editor Internals"
    ]
   },
   "delete": {
    "operationId": "leaveWorkflowPresence",
    "summary": "Clean exit: drop presence and release the lock",
    "x-permission": "workflow:read",
    "description": "The explicit counterpart to the beat: removes you from the presence set and releases the editing lock if you hold it, so the next editor does not wait out the TTL. With `?tab=` only that tab's presence and lock go; your other tabs keep theirs.\n\nIt is purely an optimization — a killed tab reaches the same state within about 30 seconds by expiry. It is idempotent and answers 200 including when you were never present (400 only for a `tab` over 64 characters). It takes no request body.",
    "parameters": [
     {
      "name": "tab",
      "in": "query",
      "required": false,
      "description": "The tab id the beats carried. Omitted = you, in every tab.",
      "schema": {
       "type": "string",
       "maxLength": 64
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Left.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "message": {
             "type": "string",
             "examples": [
              "Left"
             ]
            }
           },
           "required": [
            "message"
           ]
          }
         },
         "required": [
          "data"
         ]
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR` — `tab` is longer than 64 characters (`details.field` = `tab`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "`NOT_FOUND` — the id is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Editor Internals"
    ]
   }
  },
  "/api/workflows": {
   "get": {
    "operationId": "listWorkflows",
    "summary": "List workflows in the active workspace",
    "description": "Returns a page of workflows for the workspace the API token belongs to. Every row carries its full `definition` (the DRAFT tree), which makes this response large — a workspace of real flows is measured in hundreds of kilobytes — unless you ask for `view=summary`, which leaves out `definition` and `runs7d`. Every row carries `summary` either way (the trigger, the apps, the step count, the steps that need setup). `webhookToken` is NEVER present on a list row (the column is not selected at all), so never read a webhook URL from here; use `GET /api/workflows/{id}` with `workflow:run`. Each row says whether its draft is ahead of what is live (`hasUnpublishedChanges`), who made it (`createdBy`, `createdByName`), its latest PRODUCTION run with why it failed (`lastRun.error`), and whether it is failing (`failing`: live, and its latest FINISHED production run failed — a run after it that is waiting, running, held or was cancelled changes nothing; `failingSince`; `failingRun`, the run that decided it). A token without `run:read` is told the latest run's `status` and `startedAt` only (and the same two of `failingRun`): `lastRun.id`, `lastRun.finishedAt`, `lastRun.error`, the same three of `failingRun`, and `failingSince` come from the run itself and are left out. Asking for a page past the end returns `data: []` with the pagination block still filled in.",
    "x-permission": "workflow:read",
    "parameters": [
     {
      "name": "page",
      "in": "query",
      "required": false,
      "description": "1-based page number. Values below 1 (and non-numeric values) are silently coerced to 1.",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "default": 1
      }
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "description": "Rows per page. Anything outside 1..100 (including a non-numeric value) is silently coerced to 20.",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 20
      }
     },
     {
      "name": "q",
      "in": "query",
      "required": false,
      "description": "Case-insensitive substring match against the workflow's name, description, tags, and the apps its trigger and steps use (by app id or by the app's display name: `stripe`, `Google Sheets`). It does not search step names. `%` and `_` are escaped, so they match as literal characters.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "status",
      "in": "query",
      "required": false,
      "description": "Filter by publication state, or `failing`: live workflows whose latest FINISHED production run (one that worked or failed) failed (a paused workflow is never failing; `lastRunStatus=failed` reads the latest run whatever its state, and keeps every status). Any other value is refused with 400 VALIDATION_ERROR naming the allowed ones.",
      "schema": {
       "type": "string",
       "enum": [
        "draft",
        "active",
        "paused",
        "failing"
       ]
      }
     },
     {
      "name": "lastRunStatus",
      "in": "query",
      "required": false,
      "description": "Keep only workflows whose MOST RECENT production run ended in this state (test runs never count), whatever the workflow's own status — not \"has ever had such a run\". For the failing workflows, ask for `status=failing`. Any other value is refused with 400 VALIDATION_ERROR.",
      "schema": {
       "type": "string",
       "enum": [
        "completed",
        "failed",
        "running",
        "pending",
        "cancelled",
        "paused"
       ]
      }
     },
     {
      "name": "tag",
      "in": "query",
      "required": false,
      "description": "Keep only workflows carrying this tag, ignoring case (`prod` finds `Prod` too). Whole tag, no substring; one tag per request — there is no multi-tag form. Discover values with `GET /api/workflows/tags`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "folder",
      "in": "query",
      "required": false,
      "description": "Folder id to scope to, or the literal string `none` for workflows in no folder. Anything that is neither a UUID nor `none` returns 400 VALIDATION_ERROR.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "sort",
      "in": "query",
      "required": false,
      "description": "`name` = name A–Z; `lastrun` = most recently run first (never ran last); `status` = paused first, then active, then drafts, each by name; `updated` or omitted = most recently updated first. Any other value is refused with 400 VALIDATION_ERROR.",
      "schema": {
       "type": "string",
       "enum": [
        "updated",
        "name",
        "lastrun",
        "status"
       ]
      }
     },
     {
      "name": "ids",
      "in": "query",
      "required": false,
      "description": "Comma-separated workflow ids, at most 100: answers exactly those of the workspace's workflows (an id that is not one is left out), as one page, and every other filter is ignored. For a reader that knows which workflows it needs.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "counts",
      "in": "query",
      "required": false,
      "description": "`true` adds `counts`: the list's workflows by state and how many are failing, under the same filters (not with `ids`).",
      "schema": {
       "type": "string",
       "enum": [
        "true"
       ]
      }
     },
     {
      "name": "view",
      "in": "query",
      "required": false,
      "description": "`summary` answers each row without `definition` and `runs7d` (every row carries `summary` either way): a list's rows for a list page. Omitted = every row carries its definition. Any other value is refused with 400 VALIDATION_ERROR.",
      "schema": {
       "type": "string",
       "enum": [
        "summary"
       ]
      }
     }
    ],
    "responses": {
     "200": {
      "description": "A page of workflows.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "description": "`[]` when the requested page is past the last one.",
           "items": {
            "$ref": "#/components/schemas/Workflow"
           }
          },
          "pagination": {
           "$ref": "#/components/schemas/WorkflowPageInfo"
          },
          "counts": {
           "type": "object",
           "description": "Only with `counts=true`: the list's workflows by state, and how many of them are failing (live, and the latest finished production run failed), under the same filters as the list.",
           "required": [
            "active",
            "paused",
            "draft",
            "failing"
           ],
           "properties": {
            "active": {
             "type": "integer"
            },
            "paused": {
             "type": "integer"
            },
            "draft": {
             "type": "integer"
            },
            "failing": {
             "type": "integer"
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "A filter value this list does not know (`status`, `lastRunStatus`, `sort`, `view`); `folder` neither a UUID nor `none`; or `ids` with an entry that is not a UUID, or more than 100 (code `VALIDATION_ERROR`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   },
   "post": {
    "operationId": "createWorkflow",
    "summary": "Create a workflow",
    "description": "Creates a workflow. It is ALWAYS created as a `draft`, whatever the body says — `status`, `webhookToken`, `latestVersionId`, `workspaceId` and `orgId` are server-owned and ignored here; going live is `POST /api/workflows/{id}/publish`.\n\nOmit `definition` and you get a valid empty tree with an unconfigured `manual` trigger, ready to be filled in with `PUT /api/workflows/{id}`. Supply one and it is structurally validated first (unknown keys anywhere in the tree are refused with 400 — see the FlowVersion schema — and so are the tree errors `PUT` and publish refuse: a missing or duplicate step name, an unknown action `type`, a BRANCH with no arm), any `webhookToken` inside it is stripped, a definition without `schemaVersion` gets 2, every step without an explicit `pieceVersion` is pinned to the version the live worker fleet currently serves, a BRANCH or LOOP without a `pieceId` gets its structural one (`branch`/`evaluate`, `loop`/`loop_on_items`), a branch whose `config.branches` is empty gets its arms mirrored there, every branch gets its `Otherwise` arm last if it has none, a Branch with no `config.executionMode` gets `FIRST_MATCH` (what it runs anyway), a step or trigger without a `displayName` gets its action's, and `valid` is derived (a value sent is not kept).\n\n`triggerType` is derived from `definition.trigger` when you leave it out, so you normally should: a value you send is stored as it is, unchecked (the enum below is what the server itself writes), until the next `PUT` or publish derives it. `definition.trigger.type` is not checked either at this door, nor a `schemaVersion` other than 2 (`POST /api/workflows/validate` and publish judge what a draft holds; import refuses it). No webhook token is made here: `webhookToken` first appears on the first `PUT` of a webhook workflow, or at publish. Create does NOT check that the apps you reference exist — call `POST /api/workflows/validate` first if you want a full report before writing.",
    "x-permission": "workflow:write",
    "parameters": [
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "description": "Opt-in replay protection, max 255 characters. Retrying with the same key replays the original response (the reply then carries `Idempotency-Replayed: true`) instead of creating a second workflow. The key is fingerprinted against the method, path, query string and body: reusing it with a different request returns 422. A key whose original request is still running returns 409 after a short wait. Records are kept 24 hours.",
      "schema": {
       "type": "string",
       "maxLength": 255
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "name": {
          "type": "string",
          "maxLength": 255,
          "description": "Trimmed. Defaults to \"Untitled workflow\" when empty, spaces only or omitted."
         },
         "description": {
          "type": "string"
         },
         "definition": {
          "$ref": "#/components/schemas/FlowVersion",
          "description": "Omit for a blank draft."
         },
         "triggerType": {
          "type": "string",
          "enum": [
           "manual",
           "cron",
           "webhook",
           "callable",
           "event",
           "error",
           "run_finished",
           "poll"
          ],
          "description": "Almost always leave this out: it is derived from `definition.trigger` (falling back to `manual`). A value sent is kept as sent, unchecked, until the next `PUT` or publish derives it from the definition."
         },
         "folderId": {
          "type": [
           "string",
           "null"
          ],
          "format": "uuid",
          "description": "Folder to file the workflow under. Must be a folder in the same workspace or the create is refused with 400."
         },
         "tags": {
          "type": "array",
          "maxItems": 50,
          "items": {
           "type": "string"
          },
          "description": "Each at most 50 characters. Trimmed, blank entries dropped, deduplicated case-insensitively (the first spelling wins). More than 50 distinct tags, or one over 50 characters, is a 400."
         }
        }
       },
       "examples": {
        "blankDraft": {
         "summary": "Blank draft to fill in later",
         "value": {
          "name": "Order intake"
         }
        },
        "withDefinition": {
         "summary": "Created complete, webhook to HTTP",
         "value": {
          "name": "Order intake",
          "tags": [
           "prod"
          ],
          "definition": {
           "schemaVersion": 2,
           "trigger": {
            "name": "trigger",
            "displayName": "Order received",
            "type": "webhook",
            "pieceId": "webhook",
            "actionId": "catch_webhook",
            "config": {
             "methods": [
              "POST"
             ]
            },
            "valid": true,
            "nextAction": {
             "name": "forward",
             "displayName": "Forward to fulfilment",
             "type": "PIECE",
             "pieceId": "http",
             "actionId": "send_request",
             "config": {
              "method": "POST",
              "url": "https://example.com/fulfilment",
              "body_type": "json",
              "body_json": "{{trigger.body}}"
             },
             "valid": true
            }
           },
           "settings": {}
          }
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created. The body is the stored workflow, including the server-assigned `id` and the definition after webhook-token stripping and version pinning.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Workflow"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — unreadable JSON, a name over 255 characters, more than 50 tags or a tag over 50 characters, an unknown key in the definition tree (the message names the offending key and what is legal there), a value of the wrong type or a setting out of range (`details.field` names it), a tree error (a missing or duplicate step name, an unknown action `type`, a BRANCH with no arm), a condition operator the engine does not implement (the message names it and the valid ones, `details.field` where it sits), or a `folderId` that is not a folder in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "The token does not carry `workflow:write` (code `FORBIDDEN`). `FEATURE_OFF` (an embedded session) — `folderId` was given while the folders switch is off. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "code `CONFLICT` — an earlier request with this `Idempotency-Key` is still in flight, or it completed with a response too large to replay.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the request body exceeded the 5 MB cap.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "code `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request. `PIECE_NOT_ALLOWED` — the definition holds a step on an app this workspace may not use (a partner's customer account limited by the installation's app list, its own list, or its switches); the message names the first such step.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. Nothing is judged or served on a guess; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   }
  },
  "/api/workflows/tags": {
   "get": {
    "operationId": "listWorkflowTags",
    "summary": "List every tag in use in the workspace",
    "description": "Returns the distinct tags across all workflows in the workspace, sorted ignoring case. A tag spelled two ways (`prod`, `Prod`) is listed once, in the spelling most workflows use. Use it to discover values for the `tag` filter on `GET /api/workflows`, which ignores case. Takes no parameters. A workspace with no tags returns an empty array, never null.",
    "x-permission": "workflow:read",
    "responses": {
     "200": {
      "description": "The distinct tag list.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "type": "string"
           }
          }
         },
         "example": {
          "data": [
           "billing",
           "prod"
          ]
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   }
  },
  "/api/workflows/validate": {
   "post": {
    "operationId": "validateWorkflowDefinition",
    "summary": "Report every problem in a definition without writing anything",
    "description": "Judges a definition and returns EVERY problem it can prove, in one call. Nothing is written, nothing is created — this is the call to make BEFORE a create or an update, so you fix a whole definition at once instead of discovering faults one refused write at a time.\n\nIt answers **200 for any readable request**: a definition full of errors is a RESULT, not a transport failure. Read `ok` (nothing blocking found) and `diagnostics`, not the status code. Non-2xx is reserved for a malformed body or a workflow you cannot see.\n\nSend either `definition` (an unsaved tree you are about to POST/PUT) or `workflowId` (validate that stored workflow's current DRAFT). If both are present, `definition` is judged as that workflow's: a Sub-flow step that would run the workflow itself is reported (`subflow_self`), and every step that names no `pieceVersion` is judged at the version an update would give it — the draft's for a step the draft already has (by name), the default for a new one. Pass `workflowId` when validating an edit: a Code step you read back without its pin still runs Code 1.0.0, and is judged so. A `workflowId` beside a `definition` that names no workflow here (unknown, another workspace's or not a UUID) is ignored and the definition judged as new, and the answer says so with an `info` diagnostic, `workflow_id_ignored`. Diagnostics are capped at 200 with the dropped count in `truncated`. `skipped` names check families that could not run — most often because the worker fleet is down, which would otherwise make every app look unknown, or `code_syntax` when a Code step's compile could not be checked in time; a short diagnostics list plus a non-empty `skipped` is NOT a clean bill of health.\n\nThis runs the same validator the publish gate runs, so anything reported `blocking: true` here is exactly what a publish would refuse on.",
    "x-permission": "workflow:read",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "description": "At least one of `definition` or `workflowId` is required.",
        "properties": {
         "definition": {
          "$ref": "#/components/schemas/FlowVersion",
          "description": "The tree to judge, as you would send it to create/update."
         },
         "workflowId": {
          "type": "string",
          "format": "uuid",
          "description": "Judge this stored workflow's current DRAFT when no `definition` is sent. Beside a `definition`, the draft it would replace: unpinned steps are judged at the version the update would give them. An id that names no workflow you can read is then ignored."
         }
        }
       },
       "examples": {
        "unsavedDefinition": {
         "summary": "Judge a tree before writing it",
         "value": {
          "definition": {
           "schemaVersion": 2,
           "trigger": {
            "name": "trigger",
            "type": "webhook",
            "pieceId": "webhook",
            "actionId": "catch_webhook",
            "config": {},
            "valid": true
           },
           "settings": {}
          }
         }
        },
        "storedDraft": {
         "summary": "Judge a stored workflow's draft",
         "value": {
          "workflowId": "8a3172d2-1e17-49b3-9482-f9d97cea2848"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The report. Always returned for a readable request, whether or not the definition is valid. A step on an app this workspace may not use is reported as `piece_not_allowed`, blocking, whether or not the worker catalog is available.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WorkflowValidationReport"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — unreadable JSON, neither `definition` nor `workflowId` supplied, or a `workflowId` that is not a UUID when no `definition` is sent (beside a `definition` it is ignored).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "The token does not carry `workflow:read` (code `FORBIDDEN`). Validation deliberately needs only READ permission — judging a definition changes nothing. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no workflow with that id in this workspace, when no `definition` is sent (beside a `definition` it is ignored).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. Nothing is judged or served on a guess; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   }
  },
  "/api/code-steps/upgrade": {
   "post": {
    "operationId": "upgradeCodeStep",
    "summary": "Convert a Code 1.0.0 step's code and Inputs for Code 2.0.0",
    "description": "Code 1.0.0 pastes every `{{ }}` in its code into the program text before it runs, so data from an earlier step or the trigger can change the program. Code 2.0.0 replaces nothing inside the code: values arrive through `config.inputs` (read as `inputs.<name>`), or are read directly as `steps.<name>.<field>` and `trigger.<field>`.\n\nThis converts one step and answers the result; it reads and writes NOTHING. Send the step's `config.code` and `config.inputs`. Each `{{ }}` in the code becomes an Inputs row whose value is the original `{{ }}` text (so it resolves to the same value 1.0.0 pasted) and the code reads `inputs.<name>` instead: in code, `{{ x }}` becomes `inputs.x`; in template-literal text, `${inputs.x}`; a quoted string that is exactly one `{{ }}` becomes `inputs.x`, and one with other text becomes a template literal; in an object key, a computed key (`[inputs.x]`). A `{{ }}` in a comment is left (an `info` note); one in a regex literal, or spanning two places, is left with a `review` note to rewrite it by hand. The 1.0.0 names `$steps`, `$input`, `$trigger`, `$json` and `$now` are rewritten to what reads the same value in 2.0.0, and `$items` is declared once at the start of line 1 (`const $items = [inputs];`); no line moves. A 1.0.0 name the code declares or assigns itself, or whose 2.0.0 name (`steps`, `trigger`, `inputs`) it declares, is left as written with a `review` note.\n\nApply the answer to the step yourself — `pieceVersion: \"2.0.0\"`, `config.code` and `config.inputs` from the answer, everything else unchanged — with an ordinary workflow update. Then test the step: its result should match its last run, except where a note says otherwise. The output of a Code step stays `{result, logs}` in both versions, so later steps need no change.\n\nInputs written as one expression (a string that is not a JSON object) are refused: switch Inputs to rows and send it again. The body is read up to 600 KB, and `code` up to 256 KB. `changes` lists at most 500 rewrites; `moreChanges` counts the rest. `notes` lists at most 500 notes about the code, then the static ones; `moreNotes` counts the rest.",
    "x-permission": "workflow:read",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "code"
        ],
        "properties": {
         "code": {
          "type": "string",
          "description": "The step's `config.code`, as stored. Required; an empty string is accepted and answers `unchanged: true`."
         },
         "inputs": {
          "description": "The step's `config.inputs`: an object (the usual shape), a string holding a JSON object, or null.",
          "oneOf": [
           {
            "type": "object",
            "additionalProperties": true
           },
           {
            "type": "string"
           },
           {
            "type": "null"
           }
          ]
         }
        }
       },
       "examples": {
        "bare": {
         "summary": "A step that pastes trigger data into its code",
         "value": {
          "code": "const email = \"{{trigger.body.email}}\";\nreturn { domain: email.split(\"@\")[1] };",
          "inputs": {}
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The converted step. `unchanged: true` means the code held no `{{ }}` to convert and no 1.0.0 name, and nothing needs a `review`: upgrading only changes the pin, and the static notes still apply.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/CodeStepUpgrade"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — unreadable JSON, no `code` (`details.field`: `code`), or Inputs that cannot be converted (`details.field`: `inputs`): written as one expression, or not an object.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "The token does not carry `workflow:read` (code `FORBIDDEN`): converting writes nothing, so it needs only READ. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 600 KB, or `code` is over 256 KB, the most a Code step holds (`details.field`: `code`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   }
  },
  "/api/workflows/import": {
   "post": {
    "operationId": "importWorkflow",
    "summary": "Import a workflow — a GoRunner export, or one exported from another automation tool",
    "description": "Creates a new DRAFT workflow from the JSON that `GET /api/workflows/{id}/export` produces. The import is never activated and never reuses the exporter's identity: a new id is assigned, and any webhook token inside the definition is stripped, so the imported copy cannot receive traffic on the original's URL. It has no webhook token of its own until its first `PUT` or its publish (`webhookToken` is absent in this answer), and stays inert until you publish it. A step's secret field arrives as `[hidden secret: kept as set]` in an export; nothing here holds that value, so the field is imported empty.\n\nThe body may be the full export envelope (`{\"gorunnerExport\":\"workflow\",\"version\":1,\"workflow\":{…}}`), the bare `{name, description, triggerType, tags, definition}` object, or the whole answer of the export call (`{\"data\": <envelope>}`) — tried in that order. A name over 255 characters is shortened and tags past the limits are cut, not refused. Bodies are read up to 5 MB.\n\nUnlike `POST /api/workflows`, import ALWAYS validates every app and action reference against the live worker fleet: a definition naming an app this instance cannot run is refused with 422 rather than stored. A GoRunner export is a stored definition, so a step that names no `pieceVersion` is pinned to what it always ran: the app's OLDEST version (a Code step: `1.0.0`). Write `pieceVersion` on every step of a definition you wrote yourself (a new Code step: `2.0.0`); an import from another automation tool pins its steps itself. Structural completeness is NOT required — an in-progress export with an unset trigger imports fine as an editable draft. Connection ids inside the definition are environment-specific and are carried across verbatim; re-point them yourself after importing.\n\n**Workflows exported from other automation tools import through this same endpoint.** The format is detected from the file's own shape (a `nodes` list, or a flow with a `trigger`), translated deterministically into a GoRunner definition, and then held to everything above — the structural gate, the app check, the version pinning. The response carries an `importReport` beside the workflow saying what happened to EVERY node: `mapped` (an app here does that job), `substituted` (a generic app does the same work — an unknown HTTP-shaped node becomes the http app), or `parked` (nothing here does it, so the step exists as an invalid No Op carrying the original settings in its config, waiting to be replaced). Nothing is dropped silently.\n\nOnly the SHAPE and the genuinely portable settings carry: the steps, their order, their names, branch arms and their conditions, loop bodies, an HTTP call's method and URL, a schedule's cron, a code step's source. A code step arrives pinned to Code `2.0.0` (nothing inside its code is replaced): an `export const code = async (inputs) => { … }` wrapper is unwrapped into the function body, and later steps' references to it read its `.result`; code written against `items`, `$input`, `$json` and `$()` keeps a visible shim at the top that builds them from the step before. A code step that uses what this product does not have (npm packages, the network, Luxon dates or binary data, TypeScript) is imported not ready, and its report row says why. A trigger the translator cannot read exactly becomes MANUAL rather than a guess, because a wrong trigger is the one import mistake that acts on its own. Call `POST /api/workflows/import/preview` first to see the report before anything is created.",
    "x-permission": "workflow:write",
    "parameters": [
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "description": "Opt-in replay protection, max 255 characters. A retry with the same key replays the original response (`Idempotency-Replayed: true`) instead of importing a second copy. Reusing the key with a different request (method, path, query or body) returns 422; while the first request is still running it returns 409. Records are kept 24 hours.",
      "schema": {
       "type": "string",
       "maxLength": 255
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "oneOf": [
         {
          "$ref": "#/components/schemas/WorkflowExportEnvelope"
         },
         {
          "$ref": "#/components/schemas/WorkflowExportBody"
         }
        ]
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Imported. The body is the new draft workflow, plus `importReport` when the file came from another product.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "allOf": [
            {
             "$ref": "#/components/schemas/Workflow"
            },
            {
             "type": "object",
             "properties": {
              "importReport": {
               "$ref": "#/components/schemas/ImportReport"
              }
             }
            }
           ]
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the body is not JSON, the file could not be read, an unknown key somewhere in the definition tree (the message names the key and what is legal at that position), or a condition operator the engine does not implement (the message names it and the valid ones, `details.field` where it sits).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "409": {
      "description": "code `CONFLICT` — an earlier request with this `Idempotency-Key` is still in flight, or its response was too large to replay.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — body over the 5 MB cap.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "code `VALIDATION_ERROR` — no definition in the file, `schemaVersion` is neither 2 nor 0 or absent, a referenced app/action does not exist on this instance (the message names the step and the reference), or this `Idempotency-Key` was already used with a different request. For a GoRunner export, also `PIECE_NOT_ALLOWED` — it holds a step on an app this workspace may not use. An export from another automation tool never does: the importer maps only into the apps this workspace may use and parks the rest.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. Nothing is judged or served on a guess; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   }
  },
  "/api/workflows/import/preview": {
   "post": {
    "operationId": "previewWorkflowImport",
    "summary": "Translate a workflow exported from another automation tool and report on it, without creating anything",
    "description": "Runs the same translation `POST /api/workflows/import` runs and returns ONLY the report. Nothing is written, nothing is created — this is the call to make before an import, so a person sees \"31 mapped, 9 placeholders\" and decides, instead of finding out from a draft that has already appeared in their list.\n\nThe body is the exported file itself, verbatim. A file that is not an export this importer reads answers **200** with `format: \"\"` and a null report — not being a foreign export is an ANSWER, not a transport failure, and a GoRunner export simply goes straight to `/import`. An empty or unreadable body reads the same way. Bodies are read up to 5 MB.\n\nThe translation maps only into apps the live worker fleet can actually run, so the same file previewed on two instances can report differently. That is the point: a step naming a connector no worker carries would look finished and fail at run time.",
    "x-permission": "workflow:read",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "description": "The exported file, verbatim, as the other tool wrote it."
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The translation's report. `format` is detected from the file: `external` for another automation tool's export, or empty when the file is not an export this importer reads.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "format": {
             "type": "string",
             "description": "Detected from the file: `external` for another automation tool's export, empty when the file is not an export this importer reads.",
             "enum": [
              "external",
              ""
             ]
            },
            "report": {
             "oneOf": [
              {
               "$ref": "#/components/schemas/ImportReport"
              },
              {
               "type": "null"
              }
             ],
             "description": "Null when `format` is empty."
            },
            "message": {
             "type": "string",
             "description": "Present only when the file is neither format."
            }
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the file is larger than 5 MB.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "code `VALIDATION_ERROR` — the file names its format but could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — the translated workflow could not be built.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. Nothing is judged or served on a guess; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   }
  },
  "/api/workflows/bulk": {
   "post": {
    "operationId": "bulkUpdateWorkflows",
    "summary": "Apply one operation to many workflows",
    "description": "Applies a single operation to up to 200 workflows. It is BEST-EFFORT PER WORKFLOW and always answers 200 when the request itself is well formed: each id lands in `succeeded` or in `failed` with its own message, so one workflow that cannot be activated never blocks the rest. Always read the result body — a 200 does not mean every id worked.\n\nOperations: `activate` runs the same publish validation and trigger registration as `POST /api/workflows/{id}/publish` (a workflow that fails validation fails only its own entry); `pause` takes a live workflow offline (one that is not live fails its own entry); `delete` deletes permanently, including its runs and their stored payloads; `addTags` MERGES the given tags into each workflow's existing set (deduplicated case-insensitively, refused per-workflow past 50 tags) — it does not replace, use `PUT /api/workflows/{id}/tags` for that; `move` files each workflow into `folderId` (send `null` to un-file).\n\nA `delete` that could not be done lands in `failed` with a sentence (`Workflow not found`, or that it could not be deleted and is still there) — never in `succeeded`.\n\nThis route does NOT accept `Idempotency-Key`. Every operation it offers is naturally repeatable except `delete`, whose second attempt simply fails that id.",
    "x-permission": "workflow:write",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "ids",
         "op"
        ],
        "properties": {
         "ids": {
          "type": "array",
          "minItems": 1,
          "maxItems": 200,
          "items": {
           "type": "string",
           "format": "uuid"
          },
          "description": "Workflow ids. An entry that is not a UUID is reported in `failed`, not rejected outright."
         },
         "op": {
          "type": "string",
          "enum": [
           "activate",
           "pause",
           "delete",
           "addTags",
           "move"
          ]
         },
         "tags": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Required (and only read) when `op` is `addTags`. Must contain at least one non-blank tag or the whole request is a 400, and so must at most 50 distinct tags of at most 50 characters each."
         },
         "folderId": {
          "type": [
           "string",
           "null"
          ],
          "format": "uuid",
          "description": "Only read when `op` is `move`. `null` moves the workflows out of any folder. A folder from another workspace fails per id."
         }
        }
       },
       "examples": {
        "tagMany": {
         "summary": "Merge a tag into several workflows",
         "value": {
          "op": "addTags",
          "ids": [
           "8a3172d2-1e17-49b3-9482-f9d97cea2848"
          ],
          "tags": [
           "prod"
          ]
         }
        },
        "unfile": {
         "summary": "Move workflows out of every folder",
         "value": {
          "op": "move",
          "ids": [
           "8a3172d2-1e17-49b3-9482-f9d97cea2848"
          ],
          "folderId": null
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Per-id outcome. Both arrays are always present, possibly empty.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WorkflowBulkResult"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — unreadable body, no ids, more than 200 ids, an unknown `op`, or `addTags` with no non-blank tag or more than 50 tags.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "The token lacks a permission it needs (code `FORBIDDEN`). Every operation needs `workflow:write` (the route's own), and on top of it `workflow:delete` for `delete` and `workflow:publish` for `activate` and `pause`: a token holding only `workflow:delete` is refused here, though it may use `DELETE /api/workflows/{id}`. `FEATURE_OFF` (an embedded session) — `op: move` while the folders switch is off. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   }
  },
  "/api/workflows/{id}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id. A value that is not a UUID is a 400, not a 404.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "getWorkflow",
    "summary": "Get one workflow",
    "description": "Returns a single workflow, scoped to the token's workspace — a workflow in another workspace is a 404, never a 403.\n\n`definition` here is the DRAFT: what the editor holds, not necessarily what production runs. It is answered repaired, as every read of a definition is (versions and export too): a BRANCH or LOOP stored without its `pieceId` names its structural piece, a branch whose `config.branches` is empty has the arms mirrored there, a Branch with no `config.executionMode` reads `FIRST_MATCH` (what it runs anyway), and `valid` is derived. The stored row is not changed, and a `PUT` that sends back only that repair keeps the stored definition as it was. A field an app declares secret reads `[hidden secret: kept as set]`, for every caller; send that back on a `PUT` to keep the value. `hasUnpublishedChanges` tells you the draft has moved ahead of the published version. `definitionRev` is the optimistic-lock revision — capture it here and send it back as `expectedRev` on `PUT /api/workflows/{id}` to avoid clobbering a concurrent writer.\n\n`webhookToken` is returned ONLY to a caller holding `workflow:run`; with `workflow:read` alone the field is stripped (and a webhook workflow not yet saved by a `PUT` or published has none to return), because the token is a bearer capability against an endpoint that is unauthenticated by design — holding it IS the ability to fire the flow in production.",
    "x-permission": "workflow:read",
    "responses": {
     "200": {
      "description": "The workflow.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "allOf": [
            {
             "$ref": "#/components/schemas/Workflow"
            },
            {
             "type": "object",
             "required": [
              "sourceTemplate"
             ],
             "properties": {
              "sourceTemplate": {
               "type": [
                "object",
                "null"
               ],
               "description": "The template this workflow was made from, while it exists and the caller may see it (the gallery's own rule: this organization's, this customer workspace's, or an installation template still offered); null otherwise. Only this read carries it.",
               "required": [
                "id",
                "name"
               ],
               "properties": {
                "id": {
                 "type": "string",
                 "format": "uuid"
                },
                "name": {
                 "type": "string"
                }
               }
              },
              "triggerSubscription": {
               "description": "The trigger's registration with its provider, for a vendor webhook trigger that registers itself on publish (absent otherwise): `registering`, `active`, or `failed` with the provider's refusal in `lastError` and the next try in `nextAttemptAt`. A subscription that expires (`expiresAt`) is renewed by the workers before it does. Only this read carries it.",
               "$ref": "#/components/schemas/TriggerSubscription"
              }
             }
            }
           ]
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `id` is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "code `NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   },
   "put": {
    "operationId": "updateWorkflow",
    "summary": "Update a workflow's draft",
    "description": "Updates the DRAFT. On a published workflow this changes NOTHING in production until you call `POST /api/workflows/{id}/publish` — the live triggers keep running the published snapshot.\n\nThe update is a partial merge: an ABSENT field is left alone. `description` is the one field where an explicit `\"\"` clears it (absent = keep, empty string = clear). Sending `definition` replaces the WHOLE tree — there is no per-step patch; read the workflow, modify the tree, send it back whole.\n\nA supplied `definition` is validated in this order: unknown keys anywhere in the tree are refused (400); any `webhookToken` inside it is stripped (the token lives in its own column and must never be written through a definition); the tree is checked for a named trigger, duplicate/blank step names and legal action types (400); and if the workflow is currently ACTIVE, every app and action reference must exist on the live worker fleet (422) so a live flow cannot be silently broken. A definition without `schemaVersion` is stored as sent and reads as 0: this is still a version 2 tree, and its export imports. Steps without an explicit `pieceVersion` are then pinned to the version the fleet serves. A field an app declares secret reads `[hidden secret: kept as set]` on every read: send that back to keep the value, in the same field of the same step.\n\nServer-owned fields in the body are ignored: `status`, `webhookToken`, `latestVersionId`, `definitionRev`, `workspaceId`, `orgId`, and also `folderId` and `tags` (those have dedicated routes). Ignored does not mean unchecked: one of them with a value of the wrong type (`\"tags\": \"x\"`) makes the body unreadable, a 400, so sending back the whole workflow you read is fine and a hand-made value is not. Every accepted `PUT` bumps `definitionRev`, even one that changes nothing, and the first one on a webhook workflow makes its `webhookToken`. Writes to one workflow are serialized across the fleet, so a concurrent write may answer 409 — retry it.",
    "x-permission": "workflow:write",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "name": {
          "type": "string",
          "maxLength": 255,
          "description": "Trimmed. An empty string is ignored (the existing name is kept), not applied; spaces alone are a 400."
         },
         "description": {
          "type": "string",
          "description": "Send `\"\"` to clear it; omit to keep it."
         },
         "definition": {
          "$ref": "#/components/schemas/FlowVersion"
         },
         "triggerType": {
          "type": "string",
          "enum": [
           "manual",
           "cron",
           "webhook",
           "callable",
           "event",
           "error",
           "run_finished",
           "poll"
          ],
          "description": "Overridden whenever the definition carries a trigger — it is re-derived from `definition.trigger` after the merge, so the value sent has no lasting effect."
         },
         "expectedRev": {
          "type": "integer",
          "format": "int64",
          "minimum": 1,
          "description": "The `definitionRev` you read. When it no longer matches the stored row the save is refused with 409 instead of overwriting whoever saved in between. Omit it, or send null, 0 or a negative number, for an unconditional last-write-wins save."
         },
         "tab": {
          "type": "string",
          "maxLength": 64,
          "description": "The builder's tab id (the opaque `tab` its presence beat sends to `POST /api/workflows/{id}/presence`). With it, the save is refused with 409 `EDITING_ELSEWHERE` while the workflow's advisory editing lock is held by another tab, the same person's or someone else's, so a tab that has not yet heard it lost the lock cannot write under the one that holds it. A free lock, or one this tab holds, lets the save through, and so does presence that cannot be read. Omit it (API clients, MCP and the partner API do): the editing lock is then not consulted, as before."
         },
         "testPayloads": {
          "type": "object",
          "additionalProperties": true,
          "description": "The canvas test-fixture library, stored outside the definition so publishing or restoring a version never resets it. Absent = keep."
         },
         "pinnedData": {
          "type": "object",
          "additionalProperties": true,
          "description": "The pin library: `{\"pins\": {\"<stepName>\": {\"data\": {…}}}}`. A pinned step emits its fixture INSTEAD of executing, and only on `environment=test` runs — production always executes for real. One pin is capped at 2 MB. Absent = keep. The server re-stamps the library on every save of the definition (this update, version restore, the assistant): a pin whose step config has changed is flagged `staleAt`, not deleted, and is never replayed; a pin whose step is removed, or now runs another app, is deleted with it, so a step that later takes that name starts with no pin."
         },
         "cronExpression": {
          "type": [
           "string",
           "null"
          ],
          "description": "Stored and answered as sent, and read by nothing: the schedule runs from the trigger's own `config`, and no write derives this. Leave it out."
         }
        }
       },
       "example": {
        "name": "Order intake",
        "expectedRev": 7,
        "definition": {
         "schemaVersion": 2,
         "trigger": {
          "name": "trigger",
          "type": "webhook",
          "pieceId": "webhook",
          "actionId": "catch_webhook",
          "config": {
           "methods": [
            "POST"
           ]
          },
          "valid": true
         },
         "settings": {
          "timeoutSeconds": 120
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The stored workflow after the merge, including the bumped `definitionRev`.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Workflow"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `id` is not a UUID, the body is not JSON, a name of spaces alone or over 255 characters, a `tab` that is not text of at most 64 characters, an `expectedRev` that is not a whole number (`details.field`), a value of the wrong type or a setting out of range in the definition (`details.field`), an unknown key in the definition tree, a structurally invalid tree (unnamed trigger, duplicate step name, an action `type` that is not PIECE/CODE/BRANCH/LOOP, a BRANCH with no arms), a condition operator the engine does not implement and the stored draft does not already hold (the message names it and the valid ones, `details.field` where it sits), or a pin over 2 MB.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "code `NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "code `CONFLICT` — either `expectedRev` no longer matches the stored revision (re-read the workflow and re-apply your change; a blind retry would clobber), or another write to this same workflow held the per-flow lock past the short wait (safe to retry as-is). code `EDITING_ELSEWHERE` — only for a save that names its `tab`: the workflow's editing lock is held by another tab (\"This workflow is being edited in another tab.\" with `details.mineElsewhere: true` when it is the caller's own; \"This workflow is being edited by <name>.\" otherwise). Nothing was written.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — body over the 5 MB cap.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "code `VALIDATION_ERROR` — the workflow is ACTIVE and the new definition references an app, action or trigger the worker fleet cannot run. The message names the step and the reference. `PIECE_NOT_ALLOWED` — the definition ADDS a step on an app this workspace may not use. Steps the stored draft already holds may stay, be edited around and be deleted, so a workflow written before its account's list narrowed can still be fixed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. Nothing is judged or served on a guess; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   },
   "delete": {
    "operationId": "deleteWorkflow",
    "summary": "Delete a workflow permanently",
    "description": "Deletes the workflow and everything hanging off it: its versions, its runs (held and waiting ones included), their step rows, and the offloaded step payloads in object storage (queued in the same transaction and deleted by the workers shortly after, so the delete never waits on object storage). There is no soft delete, no undo and no confirmation step. A published workflow is deleted without being unpublished first — its webhook URL simply stops resolving.\n\nDeleting a workflow that another workflow references (as a Sub-flow target or an error workflow) is NOT refused; the referencing flow fails at run time instead. This route requires `workflow:delete`, a separate permission from `workflow:write`.",
    "x-permission": "workflow:delete",
    "responses": {
     "200": {
      "description": "Deleted.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "message": {
             "type": "string"
            }
           }
          }
         },
         "example": {
          "data": {
           "message": "Workflow deleted"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `id` is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "code `NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "code `BUSY` — something else held the workflow (a flush of one of its runs, a publish, another delete) and the delete gave up waiting for it after 30 s. Nothing was deleted and the workflow is as it was; try again in a moment.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "code `DELETE_FAILED` — the delete did not happen and the workflow is still there; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   }
  },
  "/api/workflows/{id}/export": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "exportWorkflow",
    "summary": "Export a workflow as a portable envelope",
    "description": "Returns the workflow in the portable shape `POST /api/workflows/import` accepts. It exports the DRAFT definition, not the published version, and deliberately carries only what is meaningful on another instance: name, description, trigger type, tags and definition. Ids, status, folder, run history and the webhook token are all omitted (import re-mints the token anyway).\n\nThe response is ordinary JSON in the standard `{\"data\": …}` envelope — it is not a file download and carries no `Content-Disposition`, so to write a `.json` file, save `data` (the envelope) rather than the whole response (import takes either). A field an app declares secret is exported as `[hidden secret: kept as set]`, and arrives empty on import. Connection ids inside the definition are environment-specific and travel as-is; they will not resolve on another instance until re-pointed.",
    "x-permission": "workflow:read",
    "responses": {
     "200": {
      "description": "The export envelope.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/WorkflowExportEnvelope"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `id` is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "code `NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   }
  },
  "/api/workflows/{id}/trigger-subscription": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "getWorkflowTriggerSubscription",
    "summary": "Read a workflow trigger's registration with its provider",
    "description": "The registration of a vendor webhook trigger (GitHub, Asana, Stripe, Microsoft Teams…) that registers itself with its provider when the workflow is published, alone: what the builder polls while it is being registered, without reading the whole workflow. `data` is null when the trigger registers nothing, or nothing yet (a workflow not published, or published a moment ago). The same object is `triggerSubscription` on `GET /api/workflows/{id}`. Read only: registration follows publish and unpublish; publishing again tries a failed registration at once.",
    "x-permission": "workflow:read",
    "responses": {
     "200": {
      "description": "The registration, or null.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "oneOf": [
            {
             "$ref": "#/components/schemas/TriggerSubscription"
            },
            {
             "type": "null"
            }
           ]
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `id` is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "code `NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   }
  },
  "/api/workflows/{id}/tags": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "put": {
    "operationId": "replaceWorkflowTags",
    "summary": "Replace a workflow's tags",
    "description": "REPLACES the whole tag set — send the complete list you want, not a delta; `{\"tags\": []}` clears every tag. To merge instead, use `POST /api/workflows/bulk` with `op: addTags`.\n\nThis is a dedicated column write: it never touches the definition, never mints a version and never bumps `definitionRev`, so it is safe on a live workflow and cannot conflict with an in-flight editor save. Tags are trimmed, blank entries dropped, and deduplicated case-insensitively with the first spelling kept — so `[\"Prod\", \"prod\"]` stores one tag, `Prod`. A tag is at most 50 characters. The response is a bare acknowledgement, not the workflow: read the tags back with `GET /api/workflows/{id}` if you need them.",
    "x-permission": "workflow:write",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "tags"
        ],
        "properties": {
         "tags": {
          "type": "array",
          "maxItems": 50,
          "items": {
           "type": "string"
          },
          "description": "The complete replacement set. Omitting the field, or sending null, is treated as an empty list and clears all tags (an empty BODY is unreadable: a 400)."
         }
        }
       },
       "example": {
        "tags": [
         "prod",
         "billing"
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Tags replaced.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "id": {
             "type": "string",
             "format": "uuid"
            },
            "updated": {
             "type": "boolean"
            }
           }
          }
         },
         "example": {
          "data": {
           "id": "8a3172d2-1e17-49b3-9482-f9d97cea2848",
           "updated": true
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `id` is not a UUID, the body is not JSON, or more than 50 tags, or a tag over 50 characters, were sent.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "code `NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   }
  },
  "/api/workflows/{id}/folder": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Workflow id.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "put": {
    "operationId": "moveWorkflowToFolder",
    "summary": "Move a workflow into a folder",
    "description": "Files the workflow under one folder, or un-files it. A workflow lives in at most one folder; `folderId: null` (or an omitted field) means uncategorized. The target folder must belong to the SAME workspace — a folder id from another tenant is refused with 400 `Folder not found.`, which is also what you get for a folder that does not exist at all.\n\nLike the tags route this is a dedicated column write: it does not touch the definition, mint a version or bump `definitionRev`. Folders themselves are created and listed through `/api/folders`.",
    "x-permission": "workflow:write",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "folderId": {
          "type": [
           "string",
           "null"
          ],
          "format": "uuid",
          "description": "Destination folder, or `null` to remove the workflow from every folder."
         }
        }
       },
       "examples": {
        "file": {
         "summary": "File into a folder",
         "value": {
          "folderId": "3f1b0f0a-2c4e-4b21-9b0e-6d5a2f8c1234"
         }
        },
        "unfile": {
         "summary": "Remove from any folder",
         "value": {
          "folderId": null
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Moved. Echoes the workflow id and the folder it now sits in.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "id": {
             "type": "string",
             "format": "uuid"
            },
            "folderId": {
             "type": [
              "string",
              "null"
             ],
             "format": "uuid"
            }
           }
          }
         },
         "example": {
          "data": {
           "id": "8a3172d2-1e17-49b3-9482-f9d97cea2848",
           "folderId": null
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `id` is not a UUID, the body is not JSON, or the folder does not exist in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "code `NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   }
  },
  "/api/workflows/{id}/duplicate": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "Id of the workflow to copy.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "duplicateWorkflow",
    "summary": "Duplicate a workflow",
    "description": "Copies the workflow's DRAFT definition into a brand-new workflow named `\"<original name> (Copy)\"` (when that would exceed 255 characters, the original name is shortened on a character boundary and ends in `…`, so the copy still ends in ` (Copy)`). The copy is always a `draft`, whatever the original's status, and has no webhook token until its first `PUT` or its publish, when it gets its OWN — it can never receive traffic on the original's URL, and stays inert until you publish it.\n\nThere is no request body (one sent is ignored): the new name is not settable here. Rename it afterwards with `PUT /api/workflows/{id}`. NOT copied: version history, runs, test payloads and pinned data. Copied: the definition, description, trigger type, secret variables, the folder it is filed in and its tags (so a copy made inside a folder stays there), and every step's `pieceVersion` pin and `connectionId` reference.",
    "x-permission": "workflow:write",
    "parameters": [
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "description": "Opt-in replay protection, max 255 characters. A retry with the same key replays the original 201 (`Idempotency-Replayed: true`) instead of creating a second copy. Reusing the key with a different request returns 422; while the first is still running it returns 409. Records are kept 24 hours.",
      "schema": {
       "type": "string",
       "maxLength": 255
      }
     }
    ],
    "responses": {
     "201": {
      "description": "The new draft copy.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "data": {
           "$ref": "#/components/schemas/Workflow"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `id` is not a UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "code `NOT_FOUND` — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "code `CONFLICT` — an earlier request with this `Idempotency-Key` is still in flight, or its response was too large to replay.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "code `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request. `PIECE_NOT_ALLOWED` — the definition holds a step on an app this workspace may not use (a partner's customer account limited by the installation's app list, its own list, or its switches); the message names the first such step. A copy is a new workflow, so it may carry none.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — the copy could not be written.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. Nothing is judged or served on a guess; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Workflows"
    ]
   }
  },
  "/api/connections": {
   "get": {
    "operationId": "listConnections",
    "summary": "List the workspace's connections",
    "description": "Returns every connection in the caller's workspace, newest first (ordered by createdAt DESC). Connections are WORKSPACE-scoped, never org-scoped: a connection created in another workspace is invisible here and cannot be referenced by a workflow in this one.\n\nThe stored credential is NEVER returned. What you POSTed as `config` is encrypted at rest (envelope-encrypted under a per-org key) and the Go model tags it `json:\"-\"`, so no field of any response — list, single read, create or update — ever contains it. There is no endpoint that reads a secret back. If you need the value later, keep it on your side.\n\nRows are a REDUCED projection: `workspaceId` and `orgId` appear in the JSON but are always the all-zero UUID `00000000-0000-0000-0000-000000000000`, because the query does not select those columns. The single read and an update's answer are the same row; only the create's answer carries the real ids.",
    "x-permission": "connection:read",
    "responses": {
     "200": {
      "description": "The workspace's connections. `data` is `[]` when there are none — an empty list is an empty array, never null.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/ConnectionResource"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the connections could not be listed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Connections"
    ]
   },
   "post": {
    "operationId": "createConnection",
    "summary": "Create a connection (store credentials for one app)",
    "description": "Stores a credential set for exactly ONE app. A connection is not a generic secret store: it belongs to `pieceId` (e.g. `slack`, `postgres`, `googlesheets`) and a `slack` connection can never be used by a `postgres` step.\n\nHOW TO KNOW WHAT GOES IN `config`: the credential fields are declared by the PIECE, not by this endpoint. Call `GET /api/pieces/{pieceId}` and read its `auth` object — `auth.required` says whether the app uses a connection at all, `auth.type` names the shape (`none`, `api_key`, `bearer_token`, `basic_auth`, `oauth2`, `custom`), and `auth.fields[]` lists `{name, title, type, required, description}`. Every key you put in `config` is an `auth.fields[].name`; a field of `type: \"password\"` is a secret. For an OAuth2 app, `auth.oauth2` carries `authorizationUrl`, `tokenUrl` and default `scopes`, so you normally supply only `clientId`/`clientSecret` here and then run `POST /api/connections/{id}/oauth2/start`.\n\nVALIDATION AGAINST THE FLEET: when the live worker fleet has published a catalog, `pieceId` must be an app the fleet actually runs AND that app must declare `auth.required: true` — otherwise 400 `VALIDATION_ERROR` (an unknown app, or an app that does not use a connection). `config` must give a value for every `auth.fields[]` entry with `required: true` (blank text is no value): otherwise 400 `VALIDATION_ERROR` naming them (`details.fields`, `details.field` such as `config.host`). An app waiting for its OAuth2 sign-in (created `pending`, below) is not asked, since the sign-in brings what it lacks. Keys the app does not declare are not refused; they are stored and ignored.\n\n`authType` is a free-text label recorded on the row, at most 50 characters; it is NOT checked against the app and does not change how the credential is used. Omitted, it defaults to `\"api_key\"`.\n\nSTATUS ON CREATE: normally `active`. The exception is an app completed by the authorization-code flow — the generic `oauth2` app, or any app whose `auth.oauth2` is declared (Gmail, Google Sheets, …) — with no `accessToken` in `config`: that row is created `pending`, because client credentials alone cannot call anything until the authorization flow has run.\n\nALIASED PIECES: an app whose `auth.connectionPiece` names another app (a poll trigger and its action sibling, e.g. `gmailtrigger` → `gmail`) has its connections created UNDER that app — the response echoes the canonical `pieceId`, and the same connection serves both.\n\nUSING IT FROM A WORKFLOW: reference it BY ID. A step or trigger in a workflow definition carries `\"connectionId\": \"<this id>\"` next to its `pieceId`; credentials are never inlined into a step's `config`. The connection must be in the same workspace as the workflow.\n\nThe response echoes the created row and, like every connection response, contains no credential material.\n\nOAUTH2 TOKENS OBTAINED ELSEWHERE: for an app that signs in with OAuth2, `config` may carry tokens your own sign-in got — `accessToken`, `refreshToken`, `expiresAt` (RFC 3339) and `tokenType` — with the app that issued them: `clientId`/`clientSecret` (your own app, kept for every refresh) or `oauthApp: \"installation\"` (the installation's app for the service). They refresh exactly as a sign-in's do: the connection is `oauth2`, the app's declared token URL is filled in and `expiresAt` is honoured. A refresh token without an app to refresh it is refused (400, `details.field` `config.refreshToken`), as is an `expiresAt` that is not RFC 3339. A personal access token of the person's own (a field the app marks standalone) is kept as it is.",
    "x-permission": "connection:write",
    "parameters": [
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "description": "Optional opt-in de-duplication key, 255 characters max. Retrying with the SAME key and the same method + path + query + body replays the first response verbatim without performing the write again, and the replay carries the response header `Idempotency-Replayed: true`. Records live 24 hours. The same key with a DIFFERENT request is refused 422; a duplicate arriving while the first is still running is answered 409 after a short wait. Only successful writes are recorded, so a failed call can be retried with the same key. Use a fresh UUID per logical operation.",
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "example": "7c1f5b6a-4f4b-4a8a-9c1e-9c2f3a5d7e11"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/ConnectionCreateRequest"
       },
       "examples": {
        "apiKeyPiece": {
         "summary": "API-key app — config keys come from the app's auth.fields[]",
         "value": {
          "pieceId": "seatable",
          "name": "SeaTable — orders base",
          "authType": "api_key",
          "config": {
           "serverUrl": "https://cloud.seatable.io",
           "apiToken": "..."
          }
         }
        },
        "oauth2Piece": {
         "summary": "OAuth2 app — client credentials only, then call /oauth2/start",
         "value": {
          "pieceId": "googlesheets",
          "name": "Google Sheets — finance",
          "authType": "oauth2",
          "config": {
           "clientId": "1234.apps.googleusercontent.com",
           "clientSecret": "GOCSPX-..."
          }
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created. The credential is stored encrypted and is not part of this response. With `test: true`, `test` holds the test that let it be saved.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/ConnectionResource"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — malformed JSON body; `pieceId` or `name` missing, `name` longer than 255 characters, or `authType` longer than 50 (`details.field`: `authType`); `pieceId` is not an app the live fleet runs, or the app declares no credentials; a required credential field missing from `config` (`details.fields`); OAuth2 tokens in `config` that cannot be refreshed or kept (`details.field`, such as `config.refreshToken`, or an `expiresAt` that is not RFC 3339); `oauthApp` without tokens, with `clientId`, or with no installation app for the service (`details.field: oauthApp`); or `Idempotency-Key` is longer than 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "409": {
      "description": "CONFLICT — idempotency: a request with this `Idempotency-Key` is still in flight (retry shortly), or it already completed but its response was too large to replay (fetch the resource directly).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "413": {
      "description": "PAYLOAD_TOO_LARGE — request body exceeds the 5 MB cap (surfaced only when `Idempotency-Key` is sent).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "VALIDATION_ERROR — this `Idempotency-Key` was already used with a different request. `CONNECTION_TEST_FAILED` — with `test: true`, the service refused the settings (`details.cause`); nothing was saved.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the connection could not be stored (for example the credential could not be encrypted).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`NO_VERDICT` — with `test: true`, no worker could give a verdict (`details.reason`: `no_worker`, `timeout` or `not_served`); nothing was saved, try again. `UNAVAILABLE` — the installation's OAuth app for the service (`oauthApp: \"installation\"`) could not be read; nothing was saved, try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Connections"
    ]
   }
  },
  "/api/connections/usage": {
   "get": {
    "operationId": "getConnectionUsage",
    "summary": "Map every connection to the workflows that reference it",
    "description": "Returns the reverse index `connectionId -> [{id, name}, ...]` for the caller's whole workspace — each credential's blast radius. Call it before deleting or replacing a connection to see what breaks.\n\nComputed by scanning every workflow definition in the workspace for `\"connectionId\": \"<uuid>\"`, so it covers triggers, steps, branch arms, loop bodies and error arms alike. A workflow referencing the same connection from several steps, or from both its draft and its published version, is listed ONCE, with its `status` and which of its documents name it: `inLive` (the published version, what production runs) and `inDraft`. A live workflow whose draft alone names a connection does not use it in production yet.\n\nIMPORTANT: a connection with no usage is simply ABSENT from the map — it is not present with an empty array. Treat a missing key as zero usage. A workspace where nothing references anything returns `{\"data\": {}}`. A key can also name a connection that no longer exists, deleted or never made, while a workflow still references it: such a step fails when it runs.\n\nThe scan reads each workflow's DRAFT definition and its PUBLISHED version, so a reference that survives only in the live version (edited out of the draft, not yet republished) is listed too.\n\nThis route is matched before `/api/connections/{id}`, so `usage` is not a connection id.",
    "x-permission": "connection:read",
    "responses": {
     "200": {
      "description": "The reverse usage map. Keys are connection ids; unused connections are absent.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/ConnectionUsageMap"
          }
         }
        },
        "example": {
         "data": {
          "3f1c2a44-7b9e-4a1d-9f0e-2a5c8d1b6e30": [
           {
            "id": "9a2b7c10-11d2-4c33-8a44-55e6f7089a1b",
            "name": "Order intake",
            "status": "active",
            "inLive": true,
            "inDraft": true
           }
          ]
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — usage could not be computed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Connections"
    ]
   }
  },
  "/api/connections/test": {
   "post": {
    "operationId": "testConnectionDraft",
    "summary": "Test a draft credential without saving it",
    "description": "Asks a worker to test a config typed into the add or change form. Nothing is stored or recorded. With `connectionId` the draft is laid over that connection's stored config (a blank field keeps the saved value), under the change's rule: a draft that moves a where field must send every stored secret again, else 422 `SECRETS_REQUIRED` and no worker is asked. The draft is sealed under the organization's key on its way to the worker. Answers 200 in every case that reached a verdict or not; branch on `success` and `noVerdict`.",
    "x-permission": "connection:write",
    "tags": [
     "Connections"
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/ConnectionDraftTestRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The verdict, or that none came back.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/ConnectionDraftTestResult"
          }
         }
        },
        "example": {
         "data": {
          "success": false,
          "error": "smtp auth: 535 5.7.3 Authentication unsuccessful",
          "cause": "refused"
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — a malformed body; a `connectionId` that is not a UUID; no app; an unknown app; another app than the connection's; or a `config` that cannot be encoded.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "FORBIDDEN — the caller lacks the permission. `MANAGED_CONNECTION` — (`connectionId`) the partner manages that connection. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "NOT_FOUND — `connectionId` is not in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "`SECRETS_REQUIRED` — the change moves a where field (a host, server, port, database, URL or connection string) while a stored secret was left blank. The message names the fields to type again; `details.fields` lists their keys and `details.changed` the where keys that moved. Nothing was tested or stored.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the connection's stored config could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/connections/oauth2/start": {
   "post": {
    "operationId": "startConnectionOAuth2Draft",
    "summary": "Start a sign-in that makes its connection only when it works (a window, or the API)",
    "description": "As `POST /api/connections/{id}/oauth2/start`, for a connection that does not exist yet: the callback creates it, `active` with its tokens, only once the sign-in worked. A denied, failed, cancelled or abandoned sign-in leaves nothing. Read the outcome (and the new `connectionId`) from `GET /api/connections/oauth2/attempts/{attemptId}`. With a person's session it is bound to the window that asked: `mode: tab` with `returnTo` shows the sign-in in the starting tab (see `…/go`); none is the popup.\n\nWITH AN API TOKEN (`grt_…`, `connection:write`) THIS IS THE API FLAVOUR: no GoRunner window at all. Send `{pieceId, name?, config?, returnUrl}` (or `{connectionId, returnUrl}` to sign an existing connection in again) and the answer is `{attemptId, authorizeUrl, expiresAt}`. Send your person's browser to `authorizeUrl`: it is the provider's own consent page (Google's, for Gmail), with this installation's `redirect_uri`, a single-use `state` and, where the provider supports it, an S256 PKCE challenge. When the provider sends the browser back, the callback creates the connection exactly as the dashboard's Connect does — tokens encrypted, refresh set up, `connection.create` audited, the partner's `connection.created` event — and redirects (302) to `returnUrl` with `status=connected&connectionId=…&attemptId=…`, or `status=error&reason=…&attemptId=…` (`reason`: `denied`, `service_error`, `failed`, `cancelled`, `expired`). No token or secret is ever put in that address; your own query parameters on `returnUrl` are kept. Read the outcome by API too: `GET /api/connections/oauth2/attempts/{attemptId}`, with the same token.\n\n`returnUrl` must be an absolute https address (http only for localhost) whose origin is EXACTLY this installation's own app address (the origin its dashboard is served from; `APP_URL` on a self-hosted installation), one of the installation's allowed websites (Platform › Partner keys) or, for a customer account of the embed, one of that account's own websites; a wildcard entry allows no return address. Anything else is refused before anything is stored: 400 `VALIDATION_ERROR` (not a usable address) or 422 `RETURN_URL_NOT_ALLOWED` (`details.origin`). The allowed websites are set by the installation's platform admins, so a customer's own token on GoRunner Cloud returns to the app itself, on every edition, and to nothing else.\n\nThe link is single-use, works for 10 minutes (`expiresAt`) and is bound on the server to this token, this workspace and this app: whoever completes it, the connection lands in this workspace, and only this token reads its outcome. Treat `authorizeUrl` as you would any sign-in link: give it to the person who should sign in, and to nobody else. `mode` is `redirect` or left out. Does NOT accept `Idempotency-Key`: a retry starts a new sign-in, and an unused one simply expires.\n\nAn agent (an OAuth connection on /mcp) is refused with `BROWSER_REQUIRED`.",
    "x-permission": "connection:write",
    "tags": [
     "Connections"
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "oneOf": [
         {
          "$ref": "#/components/schemas/ConnectionOAuth2DraftStartRequest"
         },
         {
          "$ref": "#/components/schemas/ConnectionOAuth2APIStartRequest"
         }
        ]
       },
       "examples": {
        "apiGmail": {
         "summary": "API token: Gmail with the installation's Google app, back to the partner's page",
         "value": {
          "pieceId": "gmail",
          "name": "Gmail · Ada",
          "returnUrl": "https://app.partner.example/integrations/google/done?user=u_123"
         }
        },
        "apiSignInAgain": {
         "summary": "API token: sign an existing connection in again",
         "value": {
          "connectionId": "0f8b2c7e-4a51-4d3e-9b2a-6c1d2e3f4a5b",
          "returnUrl": "https://app.partner.example/integrations/google/done"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The attempt: a person's window gets what binds the window to it (`ConnectionOAuth2StartResult`); an API token gets the provider's authorize URL (`ConnectionOAuth2APIStartResult`).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "oneOf": [
            {
             "$ref": "#/components/schemas/ConnectionOAuth2StartResult"
            },
            {
             "$ref": "#/components/schemas/ConnectionOAuth2APIStartResult"
            }
           ]
          }
         }
        },
        "example": {
         "data": {
          "attemptId": "Zq3XbN0iJm7dQe8TfR2wLg",
          "authorizeUrl": "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&client_id=…&code_challenge=…&code_challenge_method=S256&prompt=consent&redirect_uri=https%3A%2F%2Fapi.example.com%2Fapi%2Foauth2%2Fcallback&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F&state=…",
          "expiresAt": "2026-10-21T12:10:00Z"
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — the app, the name, the mode, `returnTo`, or (API) `returnUrl` as written, `connectionId`, or name/config sent with `connectionId`; `details.field` names it where it is one field.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "FORBIDDEN — the caller lacks the permission. `BROWSER_REQUIRED` — an agent: signing in needs a person in a browser or an API token. `MANAGED_CONNECTION` — (`connectionId`) the partner manages that connection. `PIECE_NOT_ALLOWED` — the app is not one this workspace may use. `ACCOUNT_SUSPENDED` — the workspace's account is suspended: no sign-in is started. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "NOT_FOUND — (`connectionId`) no such connection in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "`ACCOUNT_DELETED` — the workspace's account was deleted. No sign-in is started.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "VALIDATION_ERROR — no app to sign in with, or a token URL this installation does not send to. `RETURN_URL_NOT_ALLOWED` — (API) `returnUrl`'s origin is neither this installation's own app address nor one of the allowed websites; `details.origin` names it.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the sign-in could not be started, or (`connectionId`) the connection could not be read. Nothing was started.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "UNAVAILABLE — sign-ins are not available on this server, the allowed websites could not be read, or the installation's OAuth app for the service could not be read. `PIECES_UNAVAILABLE` — which apps this workspace may use could not be read. `STATE_UNAVAILABLE` — the account's state could not be read. Nothing was started.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/connections/oauth2/attempts/{attemptId}": {
   "parameters": [
    {
     "name": "attemptId",
     "in": "path",
     "required": true,
     "description": "The `attemptId` a start answered.",
     "schema": {
      "type": "string"
     }
    }
   ],
   "get": {
    "operationId": "getConnectionOAuth2AttemptAny",
    "summary": "Read how a sign-in is going",
    "description": "The same as `GET /api/connections/{id}/oauth2/attempts/{attemptId}`, for any attempt the caller started — including one that makes its connection, which has none until it connects. Only its starter, in its workspace, can read it: a person's attempt by that person, an API sign-in by the very token that started it (never by another token, nor by a session). An API sign-in answers `ConnectionOAuth2APIAttempt` — `pending`, `connected` (with `connectionId`) or `failed` (with `reason`) — and is kept for an hour after its link runs out; after that, 404.",
    "x-permission": "connection:read",
    "tags": [
     "Connections"
    ],
    "responses": {
     "200": {
      "description": "Where the sign-in stands.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "oneOf": [
            {
             "$ref": "#/components/schemas/ConnectionOAuth2Attempt"
            },
            {
             "$ref": "#/components/schemas/ConnectionOAuth2APIAttempt"
            }
           ]
          }
         }
        },
        "example": {
         "data": {
          "status": "connected",
          "connectionId": "0f8b2c7e-4a51-4d3e-9b2a-6c1d2e3f4a5b",
          "account": "ana.kovac@example.com",
          "previousAccount": "finance@fieldstone.example",
          "accountChanged": true
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "NOT_FOUND — not the caller's attempt; or an API sign-in no longer kept.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the sign-in could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "UNAVAILABLE — signing in to services isn't available on this server right now (no Redis).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/connections/oauth2/attempts/{attemptId}/cancel": {
   "parameters": [
    {
     "name": "attemptId",
     "in": "path",
     "required": true,
     "description": "The `attemptId` a start answered.",
     "schema": {
      "type": "string"
     }
    }
   ],
   "post": {
    "operationId": "cancelConnectionOAuth2Attempt",
    "summary": "Stop a sign-in",
    "description": "Ends the caller's own sign-in attempt on the server, so a sign-in finished later in a window still open connects nothing (its window says \"You stopped this sign-in\"). An attempt that already ended keeps its outcome. Only its starter can stop it: a person's attempt by that person, an API sign-in by the very token that started it. Takes no body.",
    "x-permission": "connection:write",
    "tags": [
     "Connections"
    ],
    "responses": {
     "200": {
      "description": "The attempt as it now stands: a person's as `ConnectionOAuth2Attempt`, an API sign-in (started with an API token) as `ConnectionOAuth2APIAttempt`. A sign-in that is already gone, or an id that never was one, answers `{\"status\": \"expired\"}`.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "oneOf": [
            {
             "$ref": "#/components/schemas/ConnectionOAuth2Attempt"
            },
            {
             "$ref": "#/components/schemas/ConnectionOAuth2APIAttempt"
            }
           ]
          }
         }
        },
        "example": {
         "data": {
          "status": "failed",
          "reason": "cancelled",
          "error": "You stopped this sign-in."
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "NOT_FOUND — not the caller's attempt.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the sign-in could not be read or stopped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "UNAVAILABLE — signing in to services isn't available on this server right now (no Redis).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/connections/oauth2/attempts/{attemptId}/go": {
   "parameters": [
    {
     "name": "attemptId",
     "in": "path",
     "required": true,
     "description": "The `attemptId` a start answered.",
     "schema": {
      "type": "string"
     }
    }
   ],
   "post": {
    "operationId": "launchConnectionOAuth2Here",
    "summary": "Open a sign-in in this tab",
    "description": "For an attempt started with `mode: tab` (when the browser blocks the popup): answers the provider's address and, on the same answer, sets the launch cookie the callback requires. Navigate the tab to `redirectUrl`; the callback sends it back to `returnTo` with `signin={attemptId}`. Only the attempt's starter, once. A URL completed in any other browser connects nothing.",
    "x-permission": "connection:write",
    "tags": [
     "Connections"
    ],
    "responses": {
     "200": {
      "description": "Where to send the tab.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "redirectUrl"
           ],
           "properties": {
            "redirectUrl": {
             "type": "string",
             "format": "uri"
            }
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "FORBIDDEN — the caller lacks the permission. `ACCOUNT_SUSPENDED` — the workspace's account is suspended. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "NOT_FOUND — not the caller's attempt.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "`SIGNIN_NOT_PENDING` — the attempt was opened already, ended, or is not a tab sign-in. `ACCOUNT_DELETED` — the workspace's account was deleted.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the sign-in could not be opened.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "UNAVAILABLE — signing in to services isn't available on this server right now (no Redis).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/connections/{id}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The connection's UUID.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "getConnection",
    "summary": "Read one connection's metadata",
    "description": "Returns metadata only. The stored credential is tagged `json:\"-\"` in the model and is NEVER serialized — no endpoint anywhere lets you read back a secret you wrote.\n\nIt is the list's row for this connection: `createdBy` and the creator's name are there, but `workspaceId` and `orgId` are the all-zero UUID, as on the list.\n\nA connection in another workspace answers 404, not 403: existence is not disclosed across tenants.",
    "x-permission": "connection:read",
    "responses": {
     "200": {
      "description": "The connection's metadata.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/ConnectionResource"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — `id` is not a valid UUID (\"Invalid connection ID.\").",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "NOT_FOUND — no such connection in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Connections"
    ]
   },
   "put": {
    "operationId": "updateConnection",
    "summary": "Rename a connection, or rotate its credentials",
    "description": "A full-object PUT with one deliberate exception: `config` is OPTIONAL, and its ABSENCE is meaningful.\n\n- Omit `config` → RENAME-ONLY. The stored encrypted credential is left completely untouched, and so is the verdict. This is how you rename a live connection whose secret you cannot read back.\n- Send `config` → it is MERGED over the stored config: a key left out, or a string left blank, keeps the saved value; `null` removes a key; anything else replaces it. The verdict resets (`status` `active`, no test date, no last error).\n- **A blank secret is kept only while it keeps going where it went.** When the change moves a \"where\" field — a host, server, port, database, base, token or authorize URL, a connection string, any field whose name says it addresses something (read from the app's declared fields, never a per-app list) — every stored secret must be sent again: a field of type password, a connection string, a sign-in's tokens and client secret. Otherwise the answer is 422 `SECRETS_REQUIRED`, with a sentence naming the fields to type again, and nothing is tested or stored. A rename, or a change that moves nothing, keeps the secrets.\n\n`name` is optional when `config` is sent: absent, the stored name is kept (a change of the secret need not carry the name the form was opened with, which would undo a rename made meanwhile). Sent, it is trimmed and blank is a 400; a body with neither `name` nor `config` is a 400. `authType` is optional and an empty string leaves the stored value unchanged.\n\n`pieceId` CANNOT be changed — it is not read from the body at all. To point at a different app, create a new connection.\n\n`test: true` tests the merged config first and saves only if the service accepts it.\n\nOAUTH2 TOKENS OBTAINED ELSEWHERE: for an app that signs in with OAuth2, `config` may carry tokens your own sign-in got — `accessToken`, `refreshToken`, `expiresAt` (RFC 3339) and `tokenType` — with the app that issued them: `clientId`/`clientSecret` (your own app, kept for every refresh) or `oauthApp: \"installation\"` (the installation's app for the service). They refresh exactly as a sign-in's do: the connection is `oauth2`, the app's declared token URL is filled in and `expiresAt` is honoured. A refresh token without an app to refresh it is refused (400, `details.field` `config.refreshToken`), as is an `expiresAt` that is not RFC 3339. A personal access token of the person's own (a field the app marks standalone) is kept as it is.",
    "x-permission": "connection:write",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/ConnectionUpdateRequest"
       },
       "examples": {
        "renameOnly": {
         "summary": "Rename only — credential untouched because config is omitted",
         "value": {
          "name": "Slack — ops bot"
         }
        },
        "rotateSecret": {
         "summary": "Rotate — config is merged over the stored credential",
         "value": {
          "name": "Slack — ops bot",
          "config": {
           "botToken": "xoxb-new-token"
          }
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Updated. Metadata only; no credential material. With `test: true`, `test` holds the test that let it be saved.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/ConnectionResource"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — `id` is not a valid UUID, the body is malformed JSON, `name` is blank or longer than 255 characters, `authType` is longer than 50 (`details.field`: `authType`), neither `name` nor `config` was sent, `config` could not be encoded, a change that would leave a required credential field without a value (`details.fields`; a rename alone is never refused), OAuth2 tokens in `config` that cannot be refreshed or kept (`details.field`), or `oauthApp` that does not apply (`details.field: oauthApp`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "FORBIDDEN — the token lacks `connection:write`. `MANAGED_CONNECTION` — the connection is managed by the installation's partner through its own API (`managedBy: partner`): it cannot be renamed or changed here. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "NOT_FOUND — no such connection in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "`SECRETS_REQUIRED` — the change moves a where field (a host, server, port, database, URL or connection string) while a stored secret was left blank. The message names the fields to type again; `details.fields` lists their keys and `details.changed` the where keys that moved. Nothing was tested or stored. `CONNECTION_TEST_FAILED` — with `test: true`, the service refused the merged config (`details.cause`); nothing changed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the update could not be persisted.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`NO_VERDICT` — with `test: true`, no worker could give a verdict (`details.reason`: `no_worker`, `timeout` or `not_served`); nothing was changed, try again. `UNAVAILABLE` — the installation's OAuth app for the service (`oauthApp: \"installation\"`) could not be read; nothing was changed, try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Connections"
    ]
   },
   "delete": {
    "operationId": "deleteConnection",
    "summary": "Delete a connection",
    "description": "Permanently deletes the connection and its encrypted credential. No soft delete, no undo.\n\nWorkflows are NOT checked and NOT updated: any step still carrying this `connectionId` keeps a reference that now resolves to nothing and will fail at run time. Call `GET /api/connections/usage` first to see what references it, or `POST /api/connections/{id}/replace` to repoint those workflows before deleting.\n\nThis is the one connection operation that needs the stronger `connection:delete` permission rather than `connection:write`.",
    "x-permission": "connection:delete",
    "responses": {
     "200": {
      "description": "Deleted.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/MessageAck"
          }
         }
        },
        "example": {
         "data": {
          "message": "Connection deleted"
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — `id` is not a valid UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "FORBIDDEN — the token lacks `connection:delete`. `MANAGED_CONNECTION` — the connection is managed by the installation's partner through its own API (`managedBy: partner`): it cannot be deleted here. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "NOT_FOUND — no such connection in this workspace. Any delete failure is reported this way.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Connections"
    ]
   }
  },
  "/api/connections/{id}/settings": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The connection's UUID.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "getConnectionSettings",
    "summary": "Read a connection's stored settings that are not secret",
    "description": "What the change form starts from. A read, open to readers (so it goes on while an account is suspended), but the values are answered only to someone who may change the connection (`connection:write`; in the embed, the create-connections switch too) — anyone else gets `{\"fields\": {}, \"savedSecrets\": []}` and nothing is decrypted: the stored values that are not secret (a host, a port, a user name, a switch) and the KEYS of the secrets that are saved. A secret's value is never answered, masked or not; a value that carries credentials in its address (`https://user:pass@host`) is left out too. An app the catalog declares shows only its declared fields. The read is recorded in the audit trail as a change's decrypt, not as a use.\n\nA partner-managed connection cannot be changed here: 403 `MANAGED_CONNECTION`.",
    "x-permission": "connection:read",
    "tags": [
     "Connections"
    ],
    "responses": {
     "200": {
      "description": "The non-secret settings and the saved secrets' keys.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "fields",
            "savedSecrets"
           ],
           "properties": {
            "fields": {
             "type": "object",
             "additionalProperties": true,
             "description": "The stored values that are not secret, by field name."
            },
            "savedSecrets": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "The keys of the secrets saved (never their values)."
            }
           }
          }
         }
        },
        "example": {
         "data": {
          "fields": {
           "host": "smtp.office365.com",
           "port": "587",
           "email": "warehouse@fieldstone.example",
           "tls": true
          },
          "savedSecrets": [
           "password"
          ]
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — `id` is not a valid UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "FORBIDDEN — the caller lacks `connection:read`; `MANAGED_CONNECTION` — a partner-managed connection. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "NOT_FOUND — no such connection in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the connection's stored config could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/connections/{id}/test": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The connection's UUID.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "testConnection",
    "summary": "Verify a connection's credentials against the provider",
    "description": "Asks a WORKER to run the app's own connection test against the real third-party service, then records the verdict on the row (`status` + `lastTestedAt`). The API executes no app code: only the connection id travels to the worker, which decrypts the credential itself. Takes NO request body.\n\nREAD THE BODY, NOT THE STATUS CODE. Success, provider rejection and \"no worker available\" are ALL delivered as HTTP 200. Branch on `data.success`, `data.noVerdict` and `data.unverified`. The only non-200 answers are 400/401/403/404 (bad id, auth, unknown connection) and 500 (the worker's reply could not be decoded).\n\nThree meaningful outcomes:\n- `{\"success\": true}` (optionally with `message`) — the provider accepted the credentials; the row is set to `active`.\n- `{\"success\": true, \"unverified\": true, \"message\": \"...\"}` — the app cannot be verified without running a real step, so NOTHING was proven. The row is set to `unverified`. Deliberately not a green pass.\n- `{\"success\": false, \"error\": \"...\"}` — either the provider refused (row becomes `error`, or `pending` for an OAuth2 connection that was never authorized), or NO WORKER was reachable. In the no-worker case the stored status is deliberately left UNCHANGED — \"we could not reach a worker\" is not evidence about the credentials — `error` reads \"No worker is available to run this right now...\", and `noVerdict` is true with a `reason`: branch on those, not on the words.\n\nThe call blocks up to 30 seconds (the worker's 20s provider timeout plus the hop) and returns immediately when the fleet is known to be down.\n\nDoes NOT accept `Idempotency-Key`.",
    "x-permission": "connection:write",
    "responses": {
     "200": {
      "description": "The verdict. Also the response for a failed test and for \"no worker available\" — inspect `success`.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/ConnectionTestResult"
          }
         }
        },
        "examples": {
         "verified": {
          "summary": "Provider accepted the credentials",
          "value": {
           "data": {
            "success": true,
            "message": "Connected as gorunner-bot"
           }
          }
         },
         "unverified": {
          "summary": "Nothing was actually proven",
          "value": {
           "data": {
            "success": true,
            "unverified": true,
            "message": "Credentials saved. This service is verified when the connection is first used in a run."
           }
          }
         },
         "refused": {
          "summary": "Provider refused",
          "value": {
           "data": {
            "success": false,
            "error": "invalid_auth"
           }
          }
         },
         "noWorker": {
          "summary": "No worker reachable — stored status left unchanged",
          "value": {
           "data": {
            "success": false,
            "error": "No worker is available to run this right now. Testing needs a running worker — the flow itself is unaffected.",
            "noVerdict": true,
            "reason": "no_worker"
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — `id` is not a valid UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "NOT_FOUND — no such connection in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the worker replied with something this API could not decode (\"Unreadable worker response.\").",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Connections"
    ]
   }
  },
  "/api/connections/{id}/replace": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The SOURCE connection's UUID — the one currently referenced by workflows.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "replaceConnectionReferences",
    "summary": "Repoint every workflow from this connection to another",
    "description": "Credential rotation without editing each workflow: rewrites every `\"connectionId\": \"<path id>\"` occurrence in every workflow definition in this workspace (or in `workflowIds`) to `targetId`. Triggers, steps, branch arms, loop bodies and error arms are all covered, because the rewrite is a string swap over the whole definition JSON.\n\nBOTH connections must exist in this workspace and must share the same `pieceId` — repointing a `slack` step at a `postgres` credential would silently break it, so a mismatch is refused 400. Source and target must differ.\n\nDrafts AND every stored version are rewritten, so a live workflow runs with the new connection from its next run, without being published again. Each draft's `definitionRev` moves, so an editor holding the old revision cannot save over the change.\n\nNEITHER connection is deleted. Afterwards the source still exists (now unreferenced) and can be deleted separately.\n\n`workflowIds` moves only those workflows. Returns the number of workflows updated, their ids, and which of them already named the target; `updated: 0` is a valid success meaning nothing referenced the source.\n\nDoes NOT accept `Idempotency-Key` — it is naturally idempotent, since a replay finds no remaining references and returns `updated: 0`.",
    "x-permission": "connection:write",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/ConnectionReplaceRequest"
       },
       "example": {
        "targetId": "8c7d6e5f-4a3b-2c1d-0e9f-8a7b6c5d4e3f"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "How many workflows were repointed.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/ConnectionReplaceResult"
          }
         }
        },
        "example": {
         "data": {
          "updated": 1,
          "workflowIds": [
           "9a2b7c10-11d2-4c33-8a44-55e6f7089a1b"
          ],
          "alsoOnTarget": []
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — path `id` or `targetId` is not a valid UUID; malformed body; `workflowIds` empty, longer than 500 or holding an id that is not a UUID; source and target are the same connection; or the two connections are for different apps (\"Both connections must be for the same service.\").",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "NOT_FOUND — the source (\"Connection not found.\") or the target (\"Target connection not found.\") does not exist in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the workflows could not be repointed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Connections"
    ]
   }
  },
  "/api/connections/{id}/oauth2/start": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The connection's UUID. It must already exist — create it first with the app's `clientId`/`clientSecret`.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "startConnectionOAuth2",
    "summary": "Start a sign-in for a connection, bound to the calling window",
    "description": "Starts the OAuth2 authorization-code sign-in for an existing OAuth2 connection, bound to the browser window that asked. It answers a sign-in ATTEMPT — never a provider authorize URL: a finished authorize URL can be completed by whoever it is sent to, which is how a person is tricked into putting their own account's tokens on someone else's connection. The body is optional: `{\"mode\": \"tab\", \"returnTo\": \"/connections\"}` shows the sign-in in the starting tab (`POST /api/connections/oauth2/attempts/{attemptId}/go`); none is today's popup. After the sign-in a worker tests the connection, and the attempt names who signed in (`account`, and `previousAccount` when it was someone else).\n\nCONTRACT CHANGE (2026-10): this endpoint used to return `authorizeUrl`. It no longer does: a finished authorize URL is answered only by the API flavour, to the API token that asked, bound to it on the server — `POST /api/connections/oauth2/start` with `connectionId` and `returnUrl` signs an existing connection in again from the API. Here, API tokens and agents are refused with `BROWSER_REQUIRED`.\n\nSEQUENCE, from the window that pressed Connect: (1) open a popup SYNCHRONOUSLY in the click handler (`window.open('about:blank', <a name of its own>, …)`, without `noopener` — the handshake needs `window.opener`); (2) call this; (3) point the popup at `launchUrl` and listen for `message` events whose `source` is that popup and whose `origin` is exactly `apiOrigin`; (4) the launch page sends `{protocol: \"gorunner-oauth/1\", type: \"launch.ready\", attemptId}` — answer it with `popup.postMessage({protocol, type: \"launch.bind\", attemptId, bindSecret}, apiOrigin)`; (5) once bound, the launch page severs its `window.opener` and the popup goes to the provider and back to `redirectUri`, which checks the window's launch cookie, exchanges the code (with a PKCE verifier where the provider supports one) and stores the encrypted tokens — it posts nothing to the opener; (6) poll `GET /api/connections/{id}/oauth2/attempts/{attemptId}` until `status` is `connected`, `failed` or `expired` — the only way the outcome arrives. `popup.closed` proves nothing either way, and once the popup is on the provider the opener may no longer be able to close it.\n\n`bindSecret` is single-use: keep it in memory, give it only to the popup at `apiOrigin`, never put it in a URL or storage. An attempt lasts 15 minutes (`expiresAt`); mint a new one per press.\n\nWHERE THE ENDPOINTS COME FROM: for an app that declares OAuth2 (`auth.oauth2` in `GET /api/pieces/{id}`), the authorize/token URLs and default scopes are filled in for you. For the generic `oauth2` app, the connection's own `config.authUrl`/`config.tokenUrl`/`config.scope` are used. If the instance operator configured a MANAGED OAuth app for this provider, no per-connection `clientId` is needed and the instance's client id is used — but only ever against the app's own declared authorize URL.\n\n`redirectUri` is returned so you can register it with the provider; it must match what the provider has on file or the callback fails.\n\nDoes NOT accept `Idempotency-Key`.",
    "x-permission": "connection:write",
    "responses": {
     "200": {
      "description": "A sign-in attempt bound to the calling window: where to point the popup, the secret that binds it, and the origin its messages come from.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/ConnectionOAuth2StartResult"
          }
         }
        },
        "example": {
         "data": {
          "attemptId": "Zq3XbN0iJm7dQe8TfR2wLg",
          "launchUrl": "https://api.example.com/api/oauth2/launch?attempt=Zq3XbN0iJm7dQe8TfR2wLg",
          "bindSecret": "k2Jq3w0vY8mRZp1Lx5aTbc9HdQeU4NfGs7OiWjKlVyM",
          "apiOrigin": "https://api.example.com",
          "provider": "google",
          "redirectUri": "https://api.example.com/api/oauth2/callback",
          "expiresAt": "2026-10-02T12:15:00Z"
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — `id` is not a valid UUID, the body is not JSON, `mode` is neither `popup` nor `tab`, or `returnTo` is not a path on this dashboard.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "FORBIDDEN — the token lacks `connection:write`. BROWSER_REQUIRED — the caller is an API token (which signs in through `POST /api/connections/oauth2/start` with `connectionId` and `returnUrl`) or an agent. `ACCOUNT_SUSPENDED` — the workspace's account is suspended: nothing new starts in it until it is resumed, and its data is kept. No sign-in is started. `MANAGED_CONNECTION` — the connection is managed by the installation's partner through its own API (`managedBy: partner`): the partner signs it in. `PIECE_NOT_ALLOWED` — the connection's app is not one this workspace may use; no sign-in is started. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "NOT_FOUND — no such connection in this workspace, or its stored config could not be decrypted.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "`ACCOUNT_DELETED` — the workspace's account was deleted. No sign-in is started.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "VALIDATION_ERROR — the connection has no usable `authUrl`/`clientId` (a connection of an app that does not sign in answers this too) and no managed OAuth app is configured for this provider, or the resolved `authUrl` is not a parseable URL.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the sign-in could not be started. Nothing was started.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "UNAVAILABLE — this server cannot record sign-ins right now. Also `STATE_UNAVAILABLE` — the account's state could not be read, so nothing was started; try again. Also `PIECES_UNAVAILABLE` — which apps this workspace may use could not be read; and `UNAVAILABLE` when the installation's OAuth app for this service cannot be read right now. Nothing is started either way.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Connections"
    ]
   }
  },
  "/api/connections/{id}/oauth2/attempts/{attemptId}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The connection's UUID.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    },
    {
     "name": "attemptId",
     "in": "path",
     "required": true,
     "description": "The `attemptId` that `POST /api/connections/{id}/oauth2/start` answered.",
     "schema": {
      "type": "string"
     }
    }
   ],
   "get": {
    "operationId": "getConnectionOAuth2Attempt",
    "summary": "Read how a connection sign-in is going",
    "description": "The opener's fallback for learning how a sign-in ended when the result page cannot tell it — a provider whose Cross-Origin-Opener-Policy severs the popup from its opener. Poll it every second or two while a sign-in is open.\n\nOnly its starter, in the workspace and for the connection it was started on, can read it — a person's attempt by that person, an API sign-in by the very token that started it; anyone else gets 404. A person's attempt that has aged out (15 minutes) answers `{\"status\": \"expired\"}`; an API sign-in is kept for an hour after its link runs out, and after that is 404.",
    "x-permission": "connection:read",
    "tags": [
     "Connections"
    ],
    "responses": {
     "200": {
      "description": "Where the sign-in stands: a person's as `ConnectionOAuth2Attempt`, an API sign-in (an API token signing an existing connection in again) as `ConnectionOAuth2APIAttempt`.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "oneOf": [
            {
             "$ref": "#/components/schemas/ConnectionOAuth2Attempt"
            },
            {
             "$ref": "#/components/schemas/ConnectionOAuth2APIAttempt"
            }
           ]
          }
         }
        },
        "example": {
         "data": {
          "status": "connected",
          "connectionId": "0f8b2c7e-4a51-4d3e-9b2a-6c1d2e3f4a5b"
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — `id` is not a valid UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "NOT_FOUND — no such attempt for this person, workspace and connection.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the sign-in could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "UNAVAILABLE — this server cannot read sign-ins right now.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/folders": {
   "get": {
    "operationId": "listFolders",
    "summary": "List the workspace's workflow folders",
    "description": "Returns a FLAT array of every folder in the caller's workspace, ordered case-insensitively by name. There is no nesting in the response — build the tree yourself from `parentId` (absent/null = a root folder).\n\n`workflowCount` counts workflows filed DIRECTLY in that folder; it does not include workflows in descendant folders.\n\nFolders organize workflows only; they have nothing to do with connections or templates, and a workflow's folder is set through the workflow API (`folderId`), not here.\n\nAn empty workspace returns `{\"data\": []}`.",
    "x-permission": "workflow:read",
    "responses": {
     "200": {
      "description": "The workspace's folders, flat, name-ordered.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/FolderResource"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the folders could not be listed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Folders"
    ]
   },
   "post": {
    "operationId": "createFolder",
    "summary": "Create a folder",
    "description": "Creates a folder in the caller's workspace. `name` is required and is trimmed; a name that is empty after trimming is a 400.\n\n`parentId` is optional — omit it or send null for a root folder. A `parentId` that does not exist IN THIS WORKSPACE is a 400 (\"Parent folder not found.\"); a cross-tenant id reads as not-found rather than forbidden, so folder existence is never disclosed. Nesting depth is not limited.\n\nA name must be unique among the folders in the same place, ignoring case: a sibling's name is a 409 `NAME_TAKEN`. A name is at most 255 characters.\n\nThe response carries the created folder with `workflowCount` 0.\n\nDoes NOT accept `Idempotency-Key`: a retry of a create that went through answers 409 `NAME_TAKEN`, since the folder is already there.",
    "x-permission": "workflow:write",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/FolderCreateRequest"
       },
       "examples": {
        "root": {
         "summary": "Root folder",
         "value": {
          "name": "Billing"
         }
        },
        "nested": {
         "summary": "Nested folder",
         "value": {
          "name": "Invoices",
          "parentId": "1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7081"
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/FolderResource"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — malformed body, `name` missing/blank (\"Folder name is required.\"), `name` longer than 255 characters (\"A folder name can be at most 255 characters.\"), or `parentId` does not exist in this workspace (\"Parent folder not found.\").",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "409": {
      "description": "`NAME_TAKEN` — a folder in the same place (the same parent, or the root) already has this name, ignoring case. Nothing is created.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the folder could not be created.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Folders"
    ]
   }
  },
  "/api/folders/{id}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The folder's UUID.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "put": {
    "operationId": "updateFolder",
    "summary": "Rename and/or move a folder",
    "description": "Sets the folder's `name` AND `parentId` in one write. Both fields are a true replace, and that is the trap: `parentId` is applied exactly as sent, so OMITTING it (or sending null) MOVES THE FOLDER TO THE ROOT. To rename without moving, re-send the folder's current `parentId`.\n\n`name` is required and trimmed; blank is a 400, and so is a name longer than 255 characters. A name another folder in the new place already has (ignoring case) is a 409 `NAME_TAKEN`.\n\nCycles are refused: a folder cannot be its own parent and cannot be moved into one of its own descendants. A `parentId` outside this workspace reads as \"parent folder not found\".\n\nThe response is only an acknowledgement `{id, updated: true}` — it does NOT return the updated folder. Re-list if you need the new state.",
    "x-permission": "workflow:write",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/FolderUpdateRequest"
       },
       "examples": {
        "renameKeepingParent": {
         "summary": "Rename — current parentId re-sent so the folder does not move",
         "value": {
          "name": "Invoices 2026",
          "parentId": "1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7081"
         }
        },
        "moveToRoot": {
         "summary": "Move to the root (parentId omitted)",
         "value": {
          "name": "Invoices 2026"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Acknowledged. Does not include the updated folder.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/FolderMutationAck"
          }
         }
        },
        "example": {
         "data": {
          "id": "1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7081",
          "updated": true
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — `id` is not a valid UUID, malformed body, `name` blank (\"Folder name is required.\") or longer than 255 characters (\"A folder name can be at most 255 characters.\"), \"A folder cannot be its own parent.\", \"A folder cannot move into its own descendant.\", or \"Parent folder not found.\"",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "NOT_FOUND — no such folder in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "`NAME_TAKEN` — another folder in the new place (the same parent, or the root) already has this name, ignoring case. Nothing is changed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the update could not be persisted.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Folders"
    ]
   },
   "delete": {
    "operationId": "deleteFolder",
    "summary": "Delete a folder, re-parenting its contents",
    "description": "Deletes the folder. NOTHING inside it is deleted: in one transaction its child FOLDERS and the WORKFLOWS filed in it are re-parented to this folder's own parent (to the root, if it was a root folder). Deleting a folder therefore never cascades and never orphans a workflow. A folder moved up whose name a folder in its new place already has is renamed `<name> (2)`, `(3)`, and so on, in the same transaction, so siblings never share a name.\n\nThe response is only an acknowledgement `{id, deleted: true}`.",
    "x-permission": "workflow:write",
    "responses": {
     "200": {
      "description": "Deleted; contents re-parented.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/FolderMutationAck"
          }
         }
        },
        "example": {
         "data": {
          "id": "1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7081",
          "deleted": true
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — `id` is not a valid UUID.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "NOT_FOUND — no such folder in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the delete could not be completed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Folders"
    ]
   }
  },
  "/api/templates": {
   "get": {
    "operationId": "listTemplates",
    "summary": "List the organization's workflow templates",
    "description": "Returns the templates the caller's WORKSPACE may use, as rows: everything the Templates page draws. By default each row also carries what a row carried before the Templates redesign, under the same names: the shareable `definition` and `createdBy` (absent on an installation template). `view=summary` leaves both out (the Templates page's own read; `GET /api/templates/{id}` has the definition). In an ordinary organization that is the organization's own, saved from any of its workspaces (`scope: org`). In a customer account — an organization a partner embedding this product manages — it is only those saved in THIS workspace (`scope: workspace`), and, while the license includes embedding, the templates the installation shares with its customers first (`scope: installation`, newest published first). No other organization sees an installation template, the one that shared it included.\n\nA template holding a step on an app this workspace may not use is left out. One on an app this installation does not run is listed with `available: false` and the reason. One whose definition cannot be read is left out by default; with `view=summary` it is listed only to someone who may delete it (`readable: false`).\n\nA workspace with no templates returns `{\"data\": []}`.",
    "x-permission": "workflow:read",
    "parameters": [
     {
      "name": "view",
      "in": "query",
      "required": false,
      "description": "`summary` returns the light rows: no `definition`, no `createdBy`, and the \"Can't be read\" stub for whoever may delete it. Omitted, every row also carries `definition` and `createdBy`, as before the Templates redesign. Any other non-empty value is a 400.",
      "schema": {
       "type": "string",
       "enum": [
        "summary"
       ]
      },
      "example": "summary"
     }
    ],
    "responses": {
     "200": {
      "description": "The workspace's templates, the installation's shared ones first.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/TemplateRow"
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — `view` is not `summary`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the templates could not be listed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Templates"
    ]
   },
   "post": {
    "operationId": "createTemplate",
    "summary": "Save an existing workflow's draft as a template",
    "description": "\"Save as template\". You do NOT submit a definition — you name an existing workflow (`workflowId`) and the server snapshots that workflow's CURRENT DRAFT definition. The published version is irrelevant; whatever is in the draft at this instant is captured.\n\nThe snapshot is a COPY: editing the source workflow afterwards never changes the template and deleting the source never affects it. Only `PUT /api/templates/{id}/definition` (Update steps from a workflow) replaces it, with a new copy.\n\nWHAT TRAVELS: steps and their `config`, plain workflow variables, and sticky notes. What does NOT travel: SECRET variables — they are stored outside the definition by design, so a template structurally cannot carry one. Connection references DO travel as bare ids and are resolved (or cleared) at use time — see `useTemplate`.\n\nDEFAULTS: an empty or whitespace-only `name` falls back to the source workflow's name; an empty `description` stays empty. `category` is free text with no server-side vocabulary (only trimmed). `triggerType` is copied from the source workflow and is not settable.\n\nThe workflow must be in the caller's WORKSPACE (404 otherwise). The resulting template belongs to the ORGANIZATION in a staff organization (`scope: org`), and to this WORKSPACE alone in a customer account (`scope: workspace`).\n\nDoes NOT accept `Idempotency-Key`, so a retried create makes a second template.\n\nAn empty `description` stays empty. The answer carries `blanked`: each value the copy left out. The workflow and workspace it was saved from are recorded (`sourceWorkflow`, `savedHere`). Should the new template not be readable back, the answer is `{\"id\"}` alone: it was saved.",
    "x-permission": "workflow:write",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/TemplateCreateRequest"
       },
       "example": {
        "workflowId": "9a2b7c10-11d2-4c33-8a44-55e6f7089a1b",
        "name": "Order intake",
        "description": "Webhook to parsed order, with a VIP branch",
        "category": "Sales"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created. Returns the template including the snapshotted definition.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/TemplateResource"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — malformed body, `workflowId` missing or not a UUID (\"workflowId must be a valid id.\"), or the resolved name exceeds 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "NOT_FOUND — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the template could not be saved.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Templates"
    ]
   }
  },
  "/api/templates/count": {
   "get": {
    "operationId": "countTemplates",
    "summary": "Count the templates this workspace may use",
    "description": "How many rows `GET /api/templates?view=summary` would answer the caller, judged the same way (apps, readability, scope): a template whose definition cannot be read counts for someone who may delete it, though the list without `view` leaves it out. For a door that shows only when there is one.",
    "x-permission": "workflow:read",
    "responses": {
     "200": {
      "description": "The count.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "count"
           ],
           "properties": {
            "count": {
             "type": "integer",
             "minimum": 0
            }
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the templates could not be counted.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Templates"
    ]
   }
  },
  "/api/templates/save-preview": {
   "post": {
    "operationId": "previewTemplateSave",
    "summary": "Preview saving a workflow as a template",
    "description": "Writes nothing. Answers which values saving this workflow as a template (`POST /api/templates`) would leave out of the copy, per step and field, and who would get it.",
    "x-permission": "workflow:read",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "workflowId"
        ],
        "properties": {
         "workflowId": {
          "type": "string",
          "format": "uuid"
         }
        }
       },
       "example": {
        "workflowId": "9a2b7c10-11d2-4c33-8a44-55e6f7089a1b"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "What saving would leave out.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/TemplateSavePreview"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — malformed body, or `workflowId` is not a UUID (`details.field`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "NOT_FOUND — no such workflow in this workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the workflow's definition could not be read; saving it would be refused too.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Templates"
    ]
   }
  },
  "/api/templates/{id}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The template's UUID. A value that is not a valid UUID answers 404 on the template routes, not 400.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "get": {
    "operationId": "getTemplate",
    "summary": "Read one template with its definition",
    "description": "The `GET /api/templates` row with its `definition` (the shareable snapshot). A template this workspace may not see, may not use (apps), or whose definition cannot be read answers 404.",
    "x-permission": "workflow:read",
    "responses": {
     "200": {
      "description": "The template.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/TemplateResource"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "NOT_FOUND — `id` is not a valid UUID, or no such template this workspace may use.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the template could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Templates"
    ]
   },
   "put": {
    "operationId": "updateTemplate",
    "summary": "Rename, re-describe or re-categorize a template",
    "description": "Updates ONLY the three presentation fields — `name`, `description`, `category`. The `definition` has no parameter here: `PUT /api/templates/{id}/definition` replaces it with a copy of another workflow.\n\nAll three are applied as sent (values are trimmed): omitting `description` or `category` CLEARS them rather than preserving them. Send the current values to keep them.\n\nAUTHORIZATION IS TWO-LAYER: the route needs `workflow:write`, and on top of that the handler requires the caller to be the template's CREATOR, to hold `org:manage`, or for a `workspace` template to hold `workspace:manage` in that workspace. Anyone else gets 403 even with `workflow:write`.\n\nReturns the template as re-read after the write; should that read fail, the answer is `{\"id\"}` alone (the change was made). A template whose definition cannot be read may still be renamed: the answer then has `readable: false` and no `definition`.\n\nDoes NOT accept `Idempotency-Key`.\n\nIts creator, an organization admin, or for a `workspace` template that workspace's admin may rename it. Answers the template as `GET /api/templates/{id}` does.",
    "x-permission": "workflow:write",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/TemplateUpdateRequest"
       },
       "example": {
        "name": "Order intake v2",
        "description": "Webhook to parsed order",
        "category": "Sales"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Updated. Returns the template as re-read.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/TemplateResource"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — malformed body, or `name` blank (after trimming) or over 255 characters. Nothing is written.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "FORBIDDEN — the caller is neither the template's creator nor an org admin nor, for a `workspace` template, that workspace's admin (\"Only the template's creator or an admin can change it.\"), or the token lacks `workflow:write`. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "NOT_FOUND — `id` is not a valid UUID, or no such template this workspace sees. A template the installation shares (`scope: installation`) is changed only in the Platform console and reads as not found here.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the update could not be persisted.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Templates"
    ]
   },
   "delete": {
    "operationId": "deleteTemplate",
    "summary": "Delete a template",
    "description": "Permanently deletes the template. Workflows previously created from it are UNAFFECTED — a template is a copy, never a live reference.\n\nAUTHORIZATION IS TWO-LAYER: the route needs `workflow:write`, and the handler additionally requires the caller to be the template's CREATOR, to hold `org:manage`, or for a `workspace` template to hold `workspace:manage` in that workspace.\n\nIts creator, an organization admin, or for a `workspace` template that workspace's admin may delete it.",
    "x-permission": "workflow:write",
    "responses": {
     "200": {
      "description": "Deleted.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/MessageAck"
          }
         }
        },
        "example": {
         "data": {
          "message": "Template deleted"
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "FORBIDDEN — the caller is neither the template's creator nor an org admin nor, for a `workspace` template, that workspace's admin (\"Only the template's creator or an admin can delete it.\"), or the token lacks `workflow:write`. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "NOT_FOUND — `id` is not a valid UUID, or no such template this workspace sees. A template the installation shares (`scope: installation`) is deleted only in the Platform console and reads as not found here.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the delete could not be completed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Templates"
    ]
   }
  },
  "/api/templates/{id}/definition": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The template's UUID. A value that is not a valid UUID answers 404, not 400.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "put": {
    "operationId": "replaceTemplateSteps",
    "summary": "Update a template's steps from a workflow",
    "description": "Makes one of the caller's own templates (`org` or `workspace` scope) a copy of a workflow of this workspace, as its draft stands now — blanked as a save is — keeping its name, description and category. Workflows already made from it are untouched. Only its creator, an organization admin, or for a `workspace` template that workspace's admin may. `dryRun: true` writes nothing and answers the change (`TemplateStepsChange`). Should the template not be readable back after the write, the answer is `{\"id\"}` alone: the steps were replaced.",
    "x-permission": "workflow:write",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "workflowId"
        ],
        "properties": {
         "workflowId": {
          "type": "string",
          "format": "uuid"
         },
         "dryRun": {
          "type": "boolean",
          "default": false
         }
        }
       },
       "example": {
        "workflowId": "9a2b7c10-11d2-4c33-8a44-55e6f7089a1b",
        "dryRun": true
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The template as it now stands (`TemplateResource`, with `blanked`), or with `dryRun` the change (`TemplateStepsChange`).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "oneOf": [
            {
             "$ref": "#/components/schemas/TemplateResource"
            },
            {
             "$ref": "#/components/schemas/TemplateStepsChange"
            }
           ]
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — malformed body, or `workflowId` is not a UUID (`details.field`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "FORBIDDEN — the token lacks `workflow:write`, or the caller may not manage this template. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "NOT_FOUND — no such template of this organization (an installation template is changed only in the Platform console), a template whose own definition can't be read (it can only be deleted), or no such workflow in this workspace (`details.field: workflowId`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — nothing was changed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Templates"
    ]
   }
  },
  "/api/templates/{id}/use": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The template's UUID. A value that is not a valid UUID answers 404, not 400.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "useTemplate",
    "summary": "Create a new workflow from a template",
    "description": "Creates a brand-new workflow in the caller's WORKSPACE from a template this workspace may use (`GET /api/templates`). The template is unchanged; the new workflow records it as the template it came from.\n\nBODY IS OPTIONAL. Nothing (or `{}`): a DRAFT named after the template. `name` overrides the name (trimmed; max 255 characters). `folderId` files it in one of this workspace's folders.\n\nCONNECTIONS ARE FILLED BY SLOT. A template's steps need one connection per service account — its connection SLOTS (`c1`, `c2`, … with the steps that use each; `POST /api/templates/{id}/use/preview` lists them with this workspace's connections that could fill each). `connections` maps a slot to a connection of THIS workspace made for the slot's service: `{\"c1\": \"<connectionId>\"}`; one for another service is refused 422 `CONNECTION_MISMATCH` naming the slot. A slot not named keeps the connection the template's own steps named where it still resolves here; otherwise its steps arrive with no connection, and `install.missing` lists it (`valid` stays what it says on every step, that an app and action are chosen). Any other connection reference this workspace cannot resolve is cleared the same way (the trigger, every action, branch arms, loop bodies and error arms).\n\n`publish: true` turns the new workflow on in the same request when it can run — exactly what `POST /api/workflows/{id}/publish` does — and needs `workflow:publish` (403 before anything is made). A refused turn-on is NOT an error: the draft is made and kept, the answer is still 201, and `install.publishRefusal` carries the `{code, message}` the publish endpoint would have answered.\n\nPIECES: a template holding a step on an app this workspace may not use is refused 422 `PIECE_NOT_ALLOWED`; one naming an app or a pinned app VERSION the worker fleet cannot run is refused 422 `VALIDATION_ERROR` rather than minting a workflow whose steps would fail at run time. The snapshot keeps the source flow's version pins; only steps that were never pinned pick up the fleet's current version.\n\nAccepts `Idempotency-Key`, so a retry after a dropped connection replays the first response instead of creating a second workflow.\n\nVALUES. `values` writes the person's fill-ins over the template's (`TemplateValues`). A value that points into the account the template was saved with (a dropdown filled from the account, or an input the catalog marks `bound`) arrives BLANK when the template is used outside the workspace it was saved in; an expression never does. `POST /api/templates/{id}/use/preview` lists them (`fill`).\n\nA BROKEN CONNECTION NEVER PUBLISHES. With `publish: true` and a chosen connection that is `error`, `expired` or `pending`, the draft is made (201) and `install.publishRefusal.code` is `CONNECTION_NOT_WORKING`. The connection the template's steps named is never chosen for the caller when it is broken.",
    "x-permission": "workflow:write",
    "parameters": [
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "description": "Optional opt-in de-duplication key, 255 characters max. Retrying with the SAME key and the same method + path + query + body replays the first response verbatim without creating a second workflow, and the replay carries the response header `Idempotency-Replayed: true`. Records live 24 hours. The same key with a DIFFERENT request is refused 422; a duplicate arriving while the first is still running is answered 409 after a short wait. Only successful writes are recorded. Use a fresh UUID per logical operation.",
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "example": "7c1f5b6a-4f4b-4a8a-9c1e-9c2f3a5d7e11"
     }
    ],
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/TemplateUseRequest"
       },
       "example": {
        "name": "Order intake — EU"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "The new workflow — a draft, or live when `publish` turned it on — and what the install did (`install`).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/TemplateCreatedWorkflow"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — malformed body; the resolved name exceeds 255 characters; `connections` names a slot the template does not have, or a value that is not a connection id or is not a connection of this workspace (`details.field: connections`, `details.slot`); `folderId` is not an id or not one of this workspace's folders (`details.field: folderId`); or `Idempotency-Key` is longer than 255 characters. No workflow is created. So is `values` naming a step or input the template does not have, or a value that is not JSON (`details.field: values`), and a template holding a condition operator the engine does not implement (the message names it and the valid ones).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "FORBIDDEN — the token lacks `workflow:write`, or `publish` is true and it lacks `workflow:publish`. `FEATURE_OFF` — an embedded session with the `publish` (for `publish: true`) or `folders` (for `folderId`) switch off; `details.switch` names it. `ACCOUNT_SUSPENDED` — the workspace's account is suspended: nothing new starts in it until it is resumed, and its data is kept. No workflow is created. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "NOT_FOUND — `id` is not a valid UUID, or no such template this workspace may use (another workspace's, or an installation template that is not published or not licensed).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "CONFLICT — idempotency: a request with this `Idempotency-Key` is still in flight, or it completed but its response was too large to replay. `ACCOUNT_DELETED` — the workspace's account was deleted. No workflow is created.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "413": {
      "description": "PAYLOAD_TOO_LARGE — the request body is larger than 5 MB.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "VALIDATION_ERROR — the template references an app or a pinned app version this instance's worker fleet cannot run (the message is the Templates list's \"Not available\" sentence, with `details.reason` and `details.step`), or this `Idempotency-Key` was already used with a different request. `PIECE_NOT_ALLOWED` — a step uses an app this workspace may not use. `CONNECTION_MISMATCH` — a connection named for a slot is for another service (`details.slot`). No workflow is created.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the stored template definition is invalid, the workspace's connections could not be read, or the workflow could not be created.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`STATE_UNAVAILABLE` — the account's state could not be read, so nothing was started; try again. `PIECES_UNAVAILABLE` — which apps this workspace may use could not be read; nothing was made.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Templates"
    ]
   }
  },
  "/api/templates/{id}/use/preview": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "description": "The template's UUID. A value that is not a valid UUID answers 404, not 400.",
     "schema": {
      "type": "string",
      "format": "uuid"
     }
    }
   ],
   "post": {
    "operationId": "previewTemplateUse",
    "summary": "Preview what using a template would make",
    "description": "Writes nothing. Answers what `POST /api/templates/{id}/use` would make with these `connections`: each connection SLOT with the steps that use it and this workspace's connections that could fill it (`selected` is the one that would), the diagnostics of the workflow as it would be made, and whether it could be turned on — and if not, the first reason, with the step and the field it is about. The same template, app and connection rules as `use` apply, with the same refusals.",
    "x-permission": "workflow:read",
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/TemplateUsePreviewRequest"
       },
       "example": {
        "connections": {
         "c1": "0b5e3a52-0c0e-4d2b-9a66-0f3c6b1f3a10"
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The preview.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/TemplateUsePreview"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR — malformed body; `connections` names a slot the template does not have, or a value that is not a connection id or is not a connection of this workspace (`details.field: connections`, `details.slot`); or `values` names a step or input the template does not have, or a value that is not JSON (`details.field: values`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "404": {
      "description": "NOT_FOUND — `id` is not a valid UUID, or no such template this workspace may use.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "413": {
      "description": "PAYLOAD_TOO_LARGE — the request body is larger than 5 MB.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "`PIECE_NOT_ALLOWED` — a step uses an app this workspace may not use. `CONNECTION_MISMATCH` — a connection named for a slot is for another service (`details.slot`). VALIDATION_ERROR — the template references an app or a pinned app version this instance's worker fleet cannot run (`details.reason`, `details.step`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "INTERNAL_ERROR — the template or the workspace's connections could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PIECES_UNAVAILABLE` — which apps this workspace may use could not be read.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Templates"
    ]
   }
  },
  "/api/stats/summary": {
   "get": {
    "operationId": "getStatsSummary",
    "summary": "Per-window run rollup with trend comparison",
    "x-permission": "run:read",
    "description": "The dashboard's KPI read: totals by terminal status, success rate and average duration for the chosen window, the same block for the PREVIOUS window (for deltas), the workspace's active-workflow count, and the top failing workflows. Workspace-scoped; counts runs by `started_at`.",
    "parameters": [
     {
      "name": "window",
      "in": "query",
      "required": false,
      "description": "The rolling window ending now. Unknown values fall to the `7d` default.",
      "schema": {
       "type": "string",
       "enum": [
        "24h",
        "7d",
        "30d"
       ],
       "default": "7d"
      }
     },
     {
      "name": "environment",
      "in": "query",
      "required": false,
      "description": "Which runs to count. Defaults to `production` so editor test traffic never pollutes the operational picture; `test` counts only editor-started runs; `all` counts both. Any other value counts `production`; `GET /api/runs` refuses it instead.",
      "schema": {
       "type": "string",
       "enum": [
        "production",
        "test",
        "all"
       ],
       "default": "production"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The rollup for the window.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/StatsSummary"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The aggregate could not be read. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/stats/week": {
   "get": {
    "operationId": "getStatsWeek",
    "summary": "The last seven days, by workflow and day",
    "x-permission": "run:read",
    "description": "The dashboard's week, counted over EVERY production, top-level run (a sub-flow's run is part of the run that called it, as on the Overview) that started in the seven local days ending today in `tz` (cancelled runs are not drawn): per day totals, and a line per workflow that ran, each day with its counts and its MARKS. A mark stands for `per` runs in the order they started (`per` is set by the busiest single cell, `busiest`, so every cell is drawn at most eight marks on one scale) and takes the worst outcome among them (failed > held > running > worked); a failure drawn among other outcomes says the worst of the others in `rest`; a mark of fewer than `per` runs is a thin one. `failing` lists the workflows failing by the Workflows list's rule (live, and the latest FINISHED production run failed, whenever that was), latest failure first, each with its failures in the seven days (0 when it last failed before them) and, as `latest`, the failed run that decided it. `workflows` counts the workspace's workflows by state. The answer may be up to 30 seconds old, never older than the newest run it names (`newestRunId`). The week by time, counted the same way: `hours`, every real hour of the seven days in `tz` (168, or 167/169 when the clocks change; the hours after `now` are empty), which add up to `totals`; per line, `quarters`, its runs in each local quarter-day (00–06, 06–12, 12–18, 18–24), 28 of them, which add up to its days; and `compare`, the runs from yesterday's start to `now` − 24 h and from the start of the seven days before to `now` − 7 × 24 h, so a partial today can be compared with the same span. Why runs failed: for each failing workflow (`failing`) and each that failed and then worked (`recovered`), its failed runs grouped by the step they failed at and the class of their reason (ids, UUIDs, quoted values and counts taken out, an HTTP status kept), the three biggest groups in `reasons`, the rest counted in `otherReasons`. `now` is the instant the answer was counted. `compare.previousDays` splits the seven days before by day, and `runsAtOnce.limit` is the organization's runs-at-once limit (null for none).",
    "parameters": [
     {
      "name": "tz",
      "in": "query",
      "required": false,
      "description": "The caller's time zone, an IANA name (`Europe/Berlin`); the seven days are its local days. Absent, \"Local\" or a name this server does not know: UTC; `tz` in the answer says which was used.",
      "schema": {
       "type": "string",
       "default": "UTC"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The week.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "days",
            "tz",
            "per",
            "busiest",
            "totals",
            "lines",
            "failing",
            "workflows",
            "cancelled",
            "subflowRuns",
            "now",
            "hours",
            "compare",
            "recovered",
            "runsAtOnce"
           ],
           "properties": {
            "cancelled": {
             "type": "integer",
             "description": "Production runs cancelled in the seven days. They are in no line or total (the board does not draw them)."
            },
            "subflowRuns": {
             "type": "integer",
             "description": "Production runs a Sub-flow step started in the seven days, any outcome. They are counted with the run that called them (in no line or total), while `GET /api/runs` lists each of them."
            },
            "newestRunId": {
             "type": [
              "string",
              "null"
             ],
             "format": "uuid",
             "description": "The newest production run of the last 24 hours when the week was counted (the first row of GET /api/runs?window=24h&limit=1), or null. A kept answer is reused only while this is still the newest run, so a client comparing it with the run list knows whether the week has caught up."
            },
            "days": {
             "type": "array",
             "description": "The seven local dates (YYYY-MM-DD), oldest first; the last is today.",
             "items": {
              "type": "string",
              "format": "date"
             }
            },
            "tz": {
             "type": "string"
            },
            "per": {
             "type": "integer",
             "description": "How many runs one mark stands for."
            },
            "busiest": {
             "type": "integer",
             "description": "The most runs any one workflow had on one day."
            },
            "totals": {
             "type": "array",
             "description": "Each day's runs, all workflows together.",
             "items": {
              "type": "object",
              "required": [
               "runs",
               "worked",
               "failed",
               "held",
               "running"
              ],
              "properties": {
               "runs": {
                "type": "integer"
               },
               "worked": {
                "type": "integer"
               },
               "failed": {
                "type": "integer"
               },
               "held": {
                "type": "integer"
               },
               "running": {
                "type": "integer"
               }
              }
             }
            },
            "lines": {
             "type": "array",
             "items": {
              "type": "object",
              "required": [
               "workflowId",
               "workflowName",
               "workflowStatus",
               "days",
               "quarters"
              ],
              "properties": {
               "workflowId": {
                "type": "string",
                "format": "uuid"
               },
               "workflowName": {
                "type": "string",
                "description": "The workflow's name as it stands (empty for one deleted since its runs were counted): what a reader that may not list workflows, an embed session with Workflows off, names it by."
               },
               "workflowStatus": {
                "type": "string",
                "description": "The workflow's state as it stands: active, paused or draft (empty for one deleted since). A paused workflow is never failing on the Overview."
               },
               "days": {
                "type": "array",
                "description": "Seven cells, oldest first.",
                "items": {
                 "allOf": [
                  {
                   "type": "object",
                   "required": [
                    "runs",
                    "worked",
                    "failed",
                    "held",
                    "running"
                   ],
                   "properties": {
                    "runs": {
                     "type": "integer"
                    },
                    "worked": {
                     "type": "integer"
                    },
                    "failed": {
                     "type": "integer"
                    },
                    "held": {
                     "type": "integer"
                    },
                    "running": {
                     "type": "integer"
                    }
                   }
                  },
                  {
                   "type": "object",
                   "required": [
                    "units"
                   ],
                   "properties": {
                    "units": {
                     "type": "array",
                     "description": "The cell's marks, in start order.",
                     "items": {
                      "type": "object",
                      "required": [
                       "mark",
                       "runs",
                       "rest"
                      ],
                      "properties": {
                       "mark": {
                        "type": "string",
                        "enum": [
                         "worked",
                         "failed",
                         "held",
                         "running"
                        ]
                       },
                       "runs": {
                        "type": "integer"
                       },
                       "rest": {
                        "type": [
                         "string",
                         "null"
                        ],
                        "enum": [
                         "worked",
                         "held",
                         "running",
                         null
                        ]
                       }
                      }
                     }
                    }
                   }
                  }
                 ]
                }
               },
               "quarters": {
                "type": "array",
                "minItems": 28,
                "maxItems": 28,
                "description": "The workflow's counted runs in each local quarter-day (00–06, 06–12, 12–18, 18–24) of the seven days, oldest first; day i's four add up to `days[i]`.",
                "items": {
                 "type": "object",
                 "required": [
                  "runs",
                  "failed"
                 ],
                 "properties": {
                  "runs": {
                   "type": "integer"
                  },
                  "failed": {
                   "type": "integer"
                  }
                 }
                }
               }
              }
             }
            },
            "failing": {
             "type": "array",
             "items": {
              "type": "object",
              "required": [
               "workflowId",
               "workflowName",
               "failures",
               "latest",
               "reasons",
               "otherReasons"
              ],
              "properties": {
               "workflowId": {
                "type": "string",
                "format": "uuid"
               },
               "workflowName": {
                "type": "string",
                "description": "The workflow's name as it stands (empty for one deleted since its runs were counted): what a reader that may not list workflows, an embed session with Workflows off, names it by."
               },
               "failures": {
                "type": "integer"
               },
               "latest": {
                "$ref": "#/components/schemas/Run"
               },
               "reasons": {
                "type": "array",
                "maxItems": 3,
                "description": "The workflow's failed runs in the seven days, grouped by step and class of reason, the biggest group first; at most three.",
                "items": {
                 "type": "object",
                 "required": [
                  "step",
                  "message",
                  "count",
                  "latestAt"
                 ],
                 "properties": {
                  "step": {
                   "type": [
                    "string",
                    "null"
                   ],
                   "description": "The step the runs failed at, by the name the canvas shows (its key when the workflow no longer has it); null when the message names no step."
                  },
                  "message": {
                   "type": "string",
                   "description": "The group's latest reason, without the step's prefix, at most 300 characters (the last an ellipsis when it was cut)."
                  },
                  "count": {
                   "type": "integer",
                   "description": "Failed runs in the group."
                  },
                  "latestAt": {
                   "type": "string",
                   "format": "date-time",
                   "description": "When the group's latest failed run started."
                  },
                  "connectionId": {
                   "type": "string",
                   "format": "uuid",
                   "description": "The connection the step that failed uses, in the version production runs; absent when it uses none or no step is named. A reader names a connection as a failure's cause only from this."
                  }
                 }
                }
               },
               "otherReasons": {
                "type": "integer",
                "description": "Failed runs in the groups past `reasons`: the groups' counts and this add up to `failures`."
               }
              }
             }
            },
            "workflows": {
             "type": "object",
             "required": [
              "total",
              "active",
              "paused",
              "draft"
             ],
             "properties": {
              "total": {
               "type": "integer"
              },
              "active": {
               "type": "integer"
              },
              "paused": {
               "type": "integer"
              },
              "draft": {
               "type": "integer"
              }
             }
            },
            "now": {
             "type": "string",
             "format": "date-time",
             "description": "The instant the week was counted, to the second, in `tz`'s offset: today ends here, and `compare`'s windows end a day and a week before it. A kept answer keeps its `now`."
            },
            "hours": {
             "type": "array",
             "description": "Every real hour from `days[0]` 00:00 to the end of `days[6]`, local, oldest first: 168, or 167/169 over a change of clocks. Counted runs started in each; they add up to `totals`.",
             "items": {
              "type": "object",
              "required": [
               "start",
               "runs",
               "worked",
               "failed",
               "held",
               "running"
              ],
              "properties": {
               "start": {
                "type": "string",
                "format": "date-time",
                "description": "The hour's start, in `tz`'s offset (an hour repeated when the clocks go back is told apart by its offset)."
               },
               "runs": {
                "type": "integer"
               },
               "worked": {
                "type": "integer"
               },
               "failed": {
                "type": "integer"
               },
               "held": {
                "type": "integer"
               },
               "running": {
                "type": "integer"
               }
              }
             }
            },
            "compare": {
             "type": "object",
             "required": [
              "yesterdayToNow",
              "lastWeekToNow",
              "previousDays"
             ],
             "properties": {
              "yesterdayToNow": {
               "type": "object",
               "required": [
                "runs",
                "worked",
                "failed",
                "held",
                "running"
               ],
               "properties": {
                "runs": {
                 "type": "integer"
                },
                "worked": {
                 "type": "integer"
                },
                "failed": {
                 "type": "integer"
                },
                "held": {
                 "type": "integer"
                },
                "running": {
                 "type": "integer"
                }
               },
               "description": "Counted runs started from yesterday 00:00 local to `now` − 24 h."
              },
              "lastWeekToNow": {
               "type": "object",
               "required": [
                "runs",
                "worked",
                "failed",
                "held",
                "running"
               ],
               "properties": {
                "runs": {
                 "type": "integer"
                },
                "worked": {
                 "type": "integer"
                },
                "failed": {
                 "type": "integer"
                },
                "held": {
                 "type": "integer"
                },
                "running": {
                 "type": "integer"
                }
               },
               "description": "Counted runs started from `days[0]` − 7 local days to `now` − 7 × 24 h."
              },
              "previousDays": {
               "type": "array",
               "minItems": 7,
               "maxItems": 7,
               "description": "The seven local days a week before `days`, one by one, oldest first, counted by the same rule; the last (a week before today) only up to `now` − 7 × 24 h, the same span as today so far. They add up to `lastWeekToNow`, except just after midnight following a change of clocks, when `now` − 7 × 24 h falls in the day before the last: then the last is empty.",
               "items": {
                "type": "object",
                "required": [
                 "runs",
                 "worked",
                 "failed",
                 "held",
                 "running"
                ],
                "properties": {
                 "runs": {
                  "type": "integer"
                 },
                 "worked": {
                  "type": "integer"
                 },
                 "failed": {
                  "type": "integer"
                 },
                 "held": {
                  "type": "integer"
                 },
                 "running": {
                  "type": "integer"
                 }
                }
               }
              },
              "yesterdayCovered": {
               "type": "boolean",
               "description": "Whether the records hold the whole of `yesterdayToNow`'s window: the workspace existed when it began, and run retention has not cut into it. When false the counts are no baseline and the dashboard compares nothing with yesterday. Absent from servers before 2026-10-22."
              },
              "lastWeekCovered": {
               "type": "boolean",
               "description": "Whether the records hold the whole of `lastWeekToNow`'s window (and so of `previousDays`), by the same rule. When false the dashboard says nothing against a week earlier. Absent from servers before 2026-10-22."
              }
             }
            },
            "recovered": {
             "type": "array",
             "description": "The workflows that failed in the seven days and whose latest finished production run worked, the most failures first. A paused workflow whose latest finished run failed is in neither list.",
             "items": {
              "type": "object",
              "required": [
               "workflowId",
               "workflowName",
               "failures",
               "latestAt",
               "reasons",
               "otherReasons"
              ],
              "properties": {
               "workflowId": {
                "type": "string",
                "format": "uuid"
               },
               "workflowName": {
                "type": "string",
                "description": "The workflow's name as it stands (empty for one deleted since its runs were counted): what a reader that may not list workflows, an embed session with Workflows off, names it by."
               },
               "failures": {
                "type": "integer"
               },
               "latestAt": {
                "type": "string",
                "format": "date-time",
                "description": "When its latest failed run started."
               },
               "reasons": {
                "type": "array",
                "maxItems": 3,
                "description": "The workflow's failed runs in the seven days, grouped by step and class of reason, the biggest group first; at most three.",
                "items": {
                 "type": "object",
                 "required": [
                  "step",
                  "message",
                  "count",
                  "latestAt"
                 ],
                 "properties": {
                  "step": {
                   "type": [
                    "string",
                    "null"
                   ],
                   "description": "The step the runs failed at, by the name the canvas shows (its key when the workflow no longer has it); null when the message names no step."
                  },
                  "message": {
                   "type": "string",
                   "description": "The group's latest reason, without the step's prefix, at most 300 characters (the last an ellipsis when it was cut)."
                  },
                  "count": {
                   "type": "integer",
                   "description": "Failed runs in the group."
                  },
                  "latestAt": {
                   "type": "string",
                   "format": "date-time",
                   "description": "When the group's latest failed run started."
                  },
                  "connectionId": {
                   "type": "string",
                   "format": "uuid",
                   "description": "The connection the step that failed uses, in the version production runs; absent when it uses none or no step is named. A reader names a connection as a failure's cause only from this."
                  }
                 }
                }
               },
               "otherReasons": {
                "type": "integer",
                "description": "Failed runs in the groups past `reasons`: the groups' counts and this add up to `failures`."
               }
              }
             }
            },
            "runsAtOnce": {
             "type": "object",
             "required": [
              "limit"
             ],
             "description": "The organization's runs-at-once limit, the one the workers hold its runs to (its own, else the platform's default), read live on every answer; the same number `GET /api/stats/overview`'s `limit` and a waiting run's `waitingLimit` say.",
             "properties": {
              "limit": {
               "type": [
                "integer",
                "null"
               ],
               "description": "Runs of the organization that may go at once; null when there is no limit."
              }
             }
            }
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The week could not be read. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/stats/overview": {
   "get": {
    "operationId": "getStatsOverview",
    "summary": "The Overview page: workflows, live runs, this month and the runs-at-once limit",
    "x-permission": "run:read",
    "description": "One read for the embed's Overview page (and harmless in the dashboard). For the caller's workspace: its workflows by state, its production, top-level runs over the last 7 or 30 days of the viewer's calendar (`tz`; UTC days without one — totals, success rate and one point per day, zeros included) and its latest 8 runs. For its organization: the runs this month (Platform › Usage's never-lowered count when the installation keeps one, `counted: \"usage\"`; else the runs it holds, `counted: \"runs\"`), and the runs-at-once limit with how many runs are going and waiting in line now. Editor test runs are never counted. The window's figures may be up to 30 seconds old; the limit and the latest runs are read live. In an embedded session the route needs the `overview` switch, which starts off, and `workflows` or `runs` (`FEATURE_OFF` otherwise): the Overview's figures are the data those switches allow, and with both off the `overview` switch is itself off. The embed's page no longer draws the success rate or the latest runs; both stay in this answer.",
    "parameters": [
     {
      "name": "window",
      "in": "query",
      "required": false,
      "description": "The days counted, ending today in the answer's time zone. Unknown values fall to `7d`.",
      "schema": {
       "type": "string",
       "enum": [
        "7d",
        "30d"
       ],
       "default": "7d"
      }
     },
     {
      "name": "tz",
      "in": "query",
      "required": false,
      "description": "The viewer's time zone, an IANA name (`Europe/Berlin`): the days are theirs. Absent, or a name this server does not know: UTC. `timeZone` in the answer says which was used. Runs this month stay the UTC month (Usage's count).",
      "schema": {
       "type": "string",
       "maxLength": 64
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The Overview's figures.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/StatsOverview"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "The token does not hold `run:read` (code `FORBIDDEN`). In an embedded session, `FEATURE_OFF` with `details.switch: \"overview\"` while the Overview is switched off. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The figures could not be read. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/stats/timeseries": {
   "get": {
    "operationId": "getStatsTimeseries",
    "summary": "Runs over time, bucketed by hour or day",
    "x-permission": "run:read",
    "description": "The dashboard chart's data: per-status run counts in `date_trunc` buckets over the window, gap-filled so every bucket is present. `bucket` defaults to `hour` for the 24h window and `day` otherwise.",
    "parameters": [
     {
      "name": "window",
      "in": "query",
      "required": false,
      "description": "The rolling window ending now. Unknown values fall to the `7d` default.",
      "schema": {
       "type": "string",
       "enum": [
        "24h",
        "7d",
        "30d"
       ],
       "default": "7d"
      }
     },
     {
      "name": "environment",
      "in": "query",
      "required": false,
      "description": "Which runs to count. Defaults to `production` so editor test traffic never pollutes the operational picture; `test` counts only editor-started runs; `all` counts both. Any other value counts `production`; `GET /api/runs` refuses it instead.",
      "schema": {
       "type": "string",
       "enum": [
        "production",
        "test",
        "all"
       ],
       "default": "production"
      }
     },
     {
      "name": "bucket",
      "in": "query",
      "required": false,
      "description": "Bucket unit. Unlike `window`, an unknown value is refused with 400 `VALIDATION_ERROR` — it selects the SQL truncation unit, so answering in a different unit than asked would be a wrong answer, not a default.",
      "schema": {
       "type": "string",
       "enum": [
        "hour",
        "day"
       ]
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The bucketed series.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/StatsTimeseries"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "`bucket` was neither `hour` nor `day` (code `VALIDATION_ERROR`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The aggregate could not be read. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/embed/v1/bootstrap": {
   "get": {
    "operationId": "embedBootstrap",
    "summary": "Can this installation be embedded, and here",
    "description": "Public and credential-free (`Access-Control-Allow-Origin: *`, `Vary: Origin`), for the SDK's preflight and the embedded frame. It answers only about the page asking: the browser's `Origin` when a page on another website asks, else `?origin=` (the frame names the page around it). `originAllowed` says whether that page may host the embed, and `allowedOrigins` is that page's own origin when it may, empty otherwise. Neither allowed-websites list is ever published. Revalidate with `If-None-Match` (ETag; `Cache-Control: no-cache`).",
    "parameters": [
     {
      "name": "origin",
      "in": "query",
      "required": false,
      "description": "The page to ask about, when the request comes from the installation itself (the frame). Ignored when a page on another website asks: its own `Origin` is the question.",
      "schema": {
       "type": "string"
      }
     }
    ],
    "tags": [
     "Embed: sign-in"
    ],
    "x-embed-auth": "public",
    "security": [],
    "responses": {
     "200": {
      "description": "Whether the embed is available here.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedBootstrap"
          }
         }
        }
       }
      }
     },
     "304": {
      "description": "Unchanged since the ETag sent."
     },
     "429": {
      "description": "code `RATE_LIMITED` — too many requests from this address this minute. `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — the allowed websites could not be read. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/sessions": {
   "post": {
    "operationId": "embedStartSession",
    "summary": "Exchange a login code for an embed session",
    "description": "Called by the embedded frame. The one-time login code in the body is the credential (single use, 60 s). `parentOrigin` must be one of the installation's allowed websites, or a website the partner's server allowed for the code's own account (never another account's); a Platform preview code: the installation's own web origin. The session is a bearer (`gri_…`) held in memory, 30 minutes, never a cookie; renew it at `renewAfter` with `POST /sessions/current/renew`, or with a new code and `replaces` naming the old token (ended only when it is the same person's). `turnOff` names page-level switches to turn off (unknown ids are ignored and listed in `ignored`). `me` is what `GET /api/auth/me` answers this session.",
    "tags": [
     "Embed: sign-in"
    ],
    "x-embed-auth": "code",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/EmbedSessionRequest"
       }
      }
     }
    },
    "security": [],
    "responses": {
     "201": {
      "description": "The session.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedSession"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the body is not JSON, a field has the wrong type, or `parentOrigin` is missing (`error.details.field`); the login code is not used up.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `INVALID_CODE` — the code is missing, unknown, used or expired; `EMBED_SESSION_ENDED` (`details.reason` `access_removed`, `role_changed`, `workspace_deleted`) — the code's person no longer has that access.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_AVAILABLE` (`details.reason` `not_licensed` | `sign_in_off`; the code is not consumed) or `ORIGIN_NOT_ALLOWED` (the code IS used up: ask for a new one).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — too many requests from this address this minute. `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/sessions/current": {
   "delete": {
    "operationId": "embedEndSession",
    "summary": "Sign the embed session out",
    "description": "The SDK calls it on unmount (also as a `keepalive` request from `pagehide`).",
    "tags": [
     "Embed: sign-in"
    ],
    "x-embed-auth": "session",
    "security": [
     {
      "embedSession": []
     }
    ],
    "responses": {
     "204": {
      "description": "Ended."
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no `gri_…` bearer; `EMBED_SESSION_ENDED` with `details.reason`: `expired` (also a bearer the server does not know), `ended`, `replaced`, `role_changed`, `access_removed`, `account_suspended`, `account_resumed`, `account_deleted`, `workspace_deleted`, `revoked` (the key that began it) or `website_removed`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_AVAILABLE` with `details.reason`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — too many requests from this address this minute. `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/sessions/current/turn-off": {
   "post": {
    "operationId": "embedNarrowSession",
    "summary": "Turn more switches off for this session",
    "description": "Adds page-level switches; nothing ever turns one back on. Unknown ids are ignored and listed. A bare body, or one without `turnOff`, changes nothing and answers the switches as they are.",
    "tags": [
     "Embed: sign-in"
    ],
    "x-embed-auth": "session",
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "turnOff": {
          "type": "array",
          "items": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    },
    "security": [
     {
      "embedSession": []
     }
    ],
    "responses": {
     "200": {
      "description": "The session's switches now.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedNarrowResult"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the body is not JSON, or `turnOff` is not a list of strings (`error.details.field`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no `gri_…` bearer; `EMBED_SESSION_ENDED` with `details.reason`: `expired` (also a bearer the server does not know), `ended`, `replaced`, `role_changed`, `access_removed`, `account_suspended`, `account_resumed`, `account_deleted`, `workspace_deleted`, `revoked` (the key that began it) or `website_removed`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_AVAILABLE` with `details.reason`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — too many requests from this address this minute. `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/sessions/current/renew": {
   "post": {
    "operationId": "embedRenewSession",
    "summary": "Renew this session without a new login code",
    "description": "The frame's own renewal: a new session (new bearer, another 30 minutes) with everything that bounds this one, which ends (`replaced`). Once per session; 4 a minute per line of sessions (`X-RateLimit-Scope: session`). Possible for 12 hours (the `EMBED_SESSION_MAX_HOURS` setting) after the login code that began the line (`renewableUntil`); after that, and on every refusal, the frame asks the partner's server for a login code as before. A removed person, a changed role, a deleted account or workspace end the session as on any request (401).",
    "tags": [
     "Embed: sign-in"
    ],
    "x-embed-auth": "session",
    "security": [
     {
      "embedSession": []
     }
    ],
    "responses": {
     "201": {
      "description": "The new session.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedSession"
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no `gri_…` bearer; `EMBED_SESSION_ENDED` with `details.reason`: `expired` (also a bearer the server does not know), `ended`, `replaced`, `role_changed`, `access_removed`, `account_suspended`, `account_resumed`, `account_deleted`, `workspace_deleted`, `revoked` (the key that began it) or `website_removed`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_AVAILABLE` with `details.reason`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `RENEW_REFUSED` with `details.reason`: `account_suspended`, `preview`, `max_age`, `key` (the key that began the line is revoked or past its rotation grace) or `website` (the page is no longer allowed for this account). Ask the partner's server for a login code.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — this session's renewals this minute are used up.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/login-codes": {
   "post": {
    "operationId": "embedIssueLoginCode",
    "summary": "Mint a one-time login code, provisioning just in time",
    "description": "Creates or updates the person, the account (and its default workspace), the named workspace and the membership at the role, race-safely, and answers a code the partner's page hands to the frame (single use, 60 s; never log it). A suspended account gets a code (its people can look, read-only). A changed role ends the person's older sessions in that workspace, which renew at the new role. `turnOff` applies to this login only. Not idempotency-wrapped: an unused code simply expires. Counts against the per-account rate limit of the account in the body, and against a per-person limit (20 a minute for one person of one account, the `EMBED_PERSON_RATE_LIMIT` setting): throttle your own login code route per person too.",
    "tags": [
     "Embed: sign-in"
    ],
    "x-embed-auth": "key",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/EmbedLoginCodeRequest"
       }
      }
     }
    },
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "201": {
      "description": "The code and what provisioning found or made.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedLoginCode"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller; `ORG_LIMIT` — the account is new and the license's organization limit is reached; `WORKSPACE_LIMIT` — a workspace would be made (a new account's default one, or a new `workspace.id`) and the license's workspace limit is reached. Nothing is made.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first. `BUSY` — many accounts or workspaces were being created at once under the license's limit, and this one gave up waiting for its turn: nothing was done; try again in a moment.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's, the account's or this person's login codes this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tightest window and `X-RateLimit-Scope` names it (`key`, `account` or `person`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts": {
   "get": {
    "operationId": "embedListAccounts",
    "summary": "List accounts",
    "description": "Newest first, keyset-paged.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "status",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "active",
        "suspended",
        "deleted"
       ]
      }
     },
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "The `pagination.nextCursor` of the previous page. Omit for the first page."
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 50
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "A page of accounts.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data",
          "pagination"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/EmbedAccount"
           }
          },
          "pagination": {
           "$ref": "#/components/schemas/EmbedCursorPagination"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the key's window (`X-RateLimit-Scope: key`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}": {
   "put": {
    "operationId": "embedPutAccount",
    "summary": "Create or update an account",
    "description": "Idempotent by the partner's id. `name` is required to create. `turnOff` replaces the account-level switch list (null clears it). `pieces` replaces the account's own app list — null inherits the installation's, `[]` allows only the structural apps; ids no app has are refused and named in `details.unknown`, ids outside the installation's list in `details.notAllowed`. `assistant.monthlyTokens` is the monthly assistant allowance (null = the installation default). Every field is checked before anything is written. `aiSignIn.address` is this account's own AI app sign-in address (see the field).",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/EmbedAccountInput"
       }
      }
     }
    },
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "Updated.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedAccount"
          }
         }
        }
       }
      }
     },
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedAccount"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller; `ORG_LIMIT` / `WORKSPACE_LIMIT` — creating the account (and its default workspace) would pass the license's organization or workspace limit. Nothing is made.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first. `BUSY` — many accounts or workspaces were being created at once under the license's limit, and this one gave up waiting for its turn: nothing was done; try again in a moment.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "code `SIGN_IN_ADDRESS_NOT_ALLOWED` — `aiSignIn.address`'s website is neither one of the installation's allowed websites nor one of this account's own (`details.origin`, `details.field`); add it with `POST /accounts/{accountId}/websites` first. Nothing was written.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "503": {
      "description": "code `PIECES_UNAVAILABLE` — the installation's app list could not be read; nothing was written.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   },
   "get": {
    "operationId": "embedGetAccount",
    "summary": "Get an account",
    "description": "In any status.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The account.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedAccount"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — the partner has no account with this id.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   },
   "delete": {
    "operationId": "embedDeleteAccount",
    "summary": "Delete an account (restorable for 30 days)",
    "description": "Its people's sessions end and new login codes are refused; after `purgeAfter` it is removed for good (run payloads, then every row; the account's encryption key with it). Deleting a deleted account changes nothing.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The account, deleted.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedAccount"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — the partner has no account with this id.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/suspend": {
   "post": {
    "operationId": "embedSuspendAccount",
    "summary": "Suspend an account",
    "description": "Its triggers stop (webhooks answer 503 with `Retry-After`, schedules and polls pause) and its people's sessions end (`account_suspended`) and renew read-only. It stays the partner's to manage: every write on this API still works in a suspended account (only `rerun` is refused, 409 `ACCOUNT_SUSPENDED`). Suspending a suspended account changes nothing, and keeps the first `suspendedReason`.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "description": "Retry safely: the same key (per embed API key) with the same request answers the first response again, with `Idempotency-Replayed: true`, for 24 hours; only a success is kept, so a retry after a failure is a new try. The same key with a different request is 422 `VALIDATION_ERROR`, one over 255 characters is 400 `VALIDATION_ERROR`, and 409 `CONFLICT` says a request with this key is still running: retry shortly."
     }
    ],
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "reason": {
          "type": "string",
          "maxLength": 200
         }
        }
       }
      }
     }
    },
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The account.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedAccount"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal. `VALIDATION_ERROR` — also an `Idempotency-Key` over 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — the partner has no account with this id.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first. `CONFLICT` — a request with this `Idempotency-Key` is still running; retry shortly.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "code `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/resume": {
   "post": {
    "operationId": "embedResumeAccount",
    "summary": "Resume a suspended account",
    "description": "Poll triggers are re-baselined (what arrived while suspended is not replayed); sessions open during the suspension end (`account_resumed`), and a new login code gives a writable one. Resuming an active account changes nothing.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "description": "Retry safely: the same key (per embed API key) with the same request answers the first response again, with `Idempotency-Replayed: true`, for 24 hours; only a success is kept, so a retry after a failure is a new try. The same key with a different request is 422 `VALIDATION_ERROR`, one over 255 characters is 400 `VALIDATION_ERROR`, and 409 `CONFLICT` says a request with this key is still running: retry shortly."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The account.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedAccount"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`). `VALIDATION_ERROR` — also an `Idempotency-Key` over 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — the partner has no account with this id.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first. `CONFLICT` — a request with this `Idempotency-Key` is still running; retry shortly.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "code `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/restore": {
   "post": {
    "operationId": "embedRestoreAccount",
    "summary": "Restore a deleted account",
    "description": "Within 30 days of the delete, back to the status it had. It counts against the license's organization cap again.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "description": "Retry safely: the same key (per embed API key) with the same request answers the first response again, with `Idempotency-Replayed: true`, for 24 hours; only a success is kept, so a retry after a failure is a new try. The same key with a different request is 422 `VALIDATION_ERROR`, one over 255 characters is 400 `VALIDATION_ERROR`, and 409 `CONFLICT` says a request with this key is still running: retry shortly."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The account.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedAccount"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`). `VALIDATION_ERROR` — also an `Idempotency-Key` over 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller; `ORG_LIMIT` — restoring it would pass the license's organization limit.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, or it was already purged.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_NOT_DELETED` or `RESTORE_EXPIRED`. `CONFLICT` — a request with this `Idempotency-Key` is still running; retry shortly.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "code `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/oauth-requests/{requestId}": {
   "get": {
    "operationId": "embedGetOAuthRequest",
    "summary": "Read an AI app's sign-in request",
    "description": "What an AI app sign-in request is for, after YOUR sign-in: the account (in your own id) the person must belong to, the app asking (its registered name, which it chose itself) and the host its code would go to, the address (`build` or `tools`), and whether it is still `waiting`. The installation sends the person to your AI app sign-in address with `?gr_request={requestId}` and nothing else, so read the account here, never from the link, and only after the person has signed in to your product: anyone holding an account's MCP address can start a request. Then check the person belongs to that account and ask them to confirm (“Connect Claude (claude.ai) to Bakery Ltd?”) before you call `complete`, even when they were already signed in. Not audited, and charged to the key's own `oauth-requests` budget (600 a minute, the `EMBED_OAUTH_REQUEST_RATE_LIMIT` setting), never to the login codes' window.",
    "tags": [
     "Embed: AI apps"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "requestId",
      "in": "path",
      "required": true,
      "description": "The request's id: the `gr_request` query parameter the installation sent the person to your AI app sign-in address with.",
      "schema": {
       "type": "string",
       "minLength": 16,
       "maxLength": 128
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The request.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "id",
            "account",
            "client",
            "surface",
            "status",
            "expiresAt"
           ],
           "properties": {
            "id": {
             "type": "string"
            },
            "account": {
             "type": "object",
             "required": [
              "id",
              "name"
             ],
             "properties": {
              "id": {
               "type": "string",
               "description": "Your account id."
              },
              "name": {
               "type": "string"
              }
             }
            },
            "client": {
             "type": "object",
             "required": [
              "name",
              "redirectHost",
              "kind"
             ],
             "properties": {
              "name": {
               "type": "string",
               "description": "The application's name, as its registration claims it."
              },
              "redirectHost": {
               "type": "string",
               "description": "Where its code would go: a host, or an app's callback."
              },
              "kind": {
               "type": "string",
               "enum": [
                "web",
                "loopback",
                "app"
               ]
              }
             }
            },
            "surface": {
             "type": "string",
             "enum": [
              "build",
              "tools"
             ]
            },
            "status": {
             "type": "string",
             "enum": [
              "waiting",
              "done"
             ]
            },
            "expiresAt": {
             "type": "string",
             "format": "date-time"
            }
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `OAUTH_REQUEST_NOT_FOUND` — no such request: unknown, expired (15 minutes after the app asked) or already answered and gone. It carries no `continueUrl`; tell the person “This connection request has expired. Go back to your AI app and connect again.” An installation without AI app sign-in answers a bare 404 too.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — this key's oauth-requests calls this minute (600 by default, the `EMBED_OAUTH_REQUEST_RATE_LIMIT` setting; their own window, not the one login codes use), or the request's account's calls, are used up. `Retry-After` says when it resets.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/oauth-requests/{requestId}/complete": {
   "post": {
    "operationId": "embedCompleteOAuthRequest",
    "summary": "Sign the person in for an AI app",
    "description": "Says who the person is, once they have signed in to your product, belong to the request's account and pressed Continue: the body of a login code (`user`, `account`, `workspace`, `role`, `turnOff`) plus `provision`. Answers `continueUrl`, a one-use address (60 s) to send the person's browser to with a `303`; it carries a ticket, so never log it, and redirect only to it. The installation then shows its own consent page, where the person chooses what the app may do. With `provision: false` (the default) the person must already be a member of the workspace (the account's default one when none is named) and a `role` you give must equal theirs: a sign-in never changes a membership. With `provision: true` it provisions as a login code does, and a membership it made stays if the browser never arrives. `account.id` must be the request's (read it with the GET): another account is refused and the request is not spent. Once per request. Every refusal after the request was found carries `error.details.continueUrl`, the same return without a ticket, which tells the app. The connection that follows is judged live on every request: it pauses while a switch it needs is off, and ends when the person leaves the account or a workspace it reaches, drops below this role (a lower role in a later login code or `PUT member` does this), is deactivated, when the account is deleted, or when THIS key is revoked. Not idempotency-wrapped: a retry after a success is `OAUTH_REQUEST_DONE`.",
    "tags": [
     "Embed: AI apps"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "requestId",
      "in": "path",
      "required": true,
      "description": "The request's id: the `gr_request` query parameter the installation sent the person to your AI app sign-in address with.",
      "schema": {
       "type": "string",
       "minLength": 16,
       "maxLength": 128
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "user",
         "account"
        ],
        "properties": {
         "user": {
          "type": "object",
          "required": [
           "id",
           "name"
          ],
          "properties": {
           "id": {
            "type": "string"
           },
           "name": {
            "type": "string"
           },
           "email": {
            "type": [
             "string",
             "null"
            ]
           }
          }
         },
         "account": {
          "type": "object",
          "required": [
           "id",
           "name"
          ],
          "properties": {
           "id": {
            "type": "string"
           },
           "name": {
            "type": "string"
           }
          }
         },
         "workspace": {
          "type": [
           "object",
           "null"
          ],
          "properties": {
           "id": {
            "type": "string"
           },
           "name": {
            "type": [
             "string",
             "null"
            ]
           }
          }
         },
         "role": {
          "type": "string",
          "enum": [
           "viewer",
           "operator",
           "editor"
          ],
          "description": "Without provisioning, optional, and when given it must be the person's role there. With `provision: true`, required."
         },
         "turnOff": {
          "type": "array",
          "items": {
           "type": "string"
          }
         },
         "provision": {
          "type": "boolean",
          "default": false,
          "description": "false: the person must already be a member of the workspace. true: made as a login code makes them."
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "The person is signed in for this request: send their browser to `continueUrl` (303).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "continueUrl",
            "expiresAt",
            "expiresIn",
            "account",
            "workspace",
            "user",
            "role"
           ],
           "properties": {
            "continueUrl": {
             "type": "string",
             "format": "uri"
            },
            "expiresAt": {
             "type": "string",
             "format": "date-time"
            },
            "expiresIn": {
             "type": "integer"
            },
            "account": {
             "type": "object",
             "required": [
              "id",
              "status",
              "created"
             ],
             "properties": {
              "id": {
               "type": "string"
              },
              "status": {
               "type": "string",
               "enum": [
                "active",
                "suspended"
               ]
              },
              "created": {
               "type": "boolean"
              }
             }
            },
            "workspace": {
             "type": "object",
             "required": [
              "id",
              "default",
              "created"
             ],
             "properties": {
              "id": {
               "type": [
                "string",
                "null"
               ]
              },
              "default": {
               "type": "boolean"
              },
              "created": {
               "type": "boolean"
              }
             }
            },
            "user": {
             "type": "object",
             "required": [
              "id",
              "created"
             ],
             "properties": {
              "id": {
               "type": "string"
              },
              "created": {
               "type": "boolean"
              }
             }
            },
            "role": {
             "type": "string",
             "enum": [
              "viewer",
              "operator",
              "editor"
             ]
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field, as for a login code.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller; `WORKSPACE_LIMIT` — with `provision: true`, a new workspace would pass the license's workspace limit; `FEATURE_OFF` — a switch AI app sign-in needs is off for this account, or in `turnOff` (`error.details.switch`: `tokens`, `mcp`, `mcpSignIn`, or `mcpBuild` for a build address). Carries `error.details.continueUrl`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `OAUTH_REQUEST_NOT_FOUND` — no such request (unknown or expired); no `continueUrl`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `OAUTH_REQUEST_ACCOUNT_MISMATCH` — `account.id` is not the request's (`error.details.expected` names it); the request is not spent. `OAUTH_REQUEST_DONE` — it was already completed or denied. `NOT_A_MEMBER` — with `provision: false`, the person is not a member of the workspace. `ROLE_DIFFERS` — with `provision: false`, `role` is not their role there. `ACCOUNT_DELETED` — the account is deleted. Each carries `error.details.continueUrl`. `ROLE_DIFFERS` carries `error.details.role`, the role they hold.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's `oauth-requests` budget, the account's or this person's (the `EMBED_PERSON_RATE_LIMIT` setting, shared with login codes) is used up this minute. `Retry-After` says when.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/oauth-requests/{requestId}/deny": {
   "post": {
    "operationId": "embedDenyOAuthRequest",
    "summary": "Turn an AI app's sign-in request down",
    "description": "Ends the request when the person pressed Cancel, does not belong to its account, or you refuse them: `reason` is `cancelled`, `not_member`, `not_allowed` or `role` (how you keep viewers out: both viewers and operators may connect otherwise, read-only and read-and-run). Answers `continueUrl`: send the person's browser there with a `303` and the app is told it was not allowed. Once per request. Audited `embed.oauth.denied`.",
    "tags": [
     "Embed: AI apps"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "requestId",
      "in": "path",
      "required": true,
      "description": "The request's id: the `gr_request` query parameter the installation sent the person to your AI app sign-in address with.",
      "schema": {
       "type": "string",
       "minLength": 16,
       "maxLength": 128
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "reason": {
          "type": "string",
          "enum": [
           "not_member",
           "not_allowed",
           "role",
           "cancelled"
          ],
          "default": "cancelled"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Denied: send the browser to `continueUrl` (303).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "continueUrl"
           ],
           "properties": {
            "continueUrl": {
             "type": "string",
             "format": "uri"
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `reason` is not one of the four, or the body is not a JSON object.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `OAUTH_REQUEST_NOT_FOUND` — no such request (unknown or expired); no `continueUrl`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `OAUTH_REQUEST_DONE` — it was already completed or denied; carries `error.details.continueUrl`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — this key's oauth-requests calls this minute (600 by default, the `EMBED_OAUTH_REQUEST_RATE_LIMIT` setting; their own window, not the one login codes use), or the request's account's calls, are used up. `Retry-After` says when it resets.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/ai-apps": {
   "get": {
    "operationId": "embedListAIApps",
    "summary": "List the AI apps connected to an account",
    "description": "The AI apps people of the account connected by signing in (not the account's API tokens): the app, the person, the workspaces it reaches, what it may do (`access`: `read`, `run`, `draft` or `publish`), the address, and `paused` — the first switch that stops it right now (`tokens`, `mcp`, `mcpSignIn`, then `mcpBuild` for a build connection), or null. A paused connection works again when the switch is back on, for up to 180 days after it was connected. `user` lists one person's only. Paged with `limit` and `cursor`; `next` (this list's `pagination.nextCursor`) is null on the last page, and a full page can carry one with nothing after it. Workspaces here name the default one `default`, not null.",
    "tags": [
     "Embed: AI apps"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "user",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      },
      "description": "Only this person's (your user id). A person with none, or one who does not exist, is an empty list."
     },
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "The `next` of the previous page. Omit for the first page."
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 50
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The connections, newest first.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data",
          "next",
          "pagination"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "type": "object",
            "required": [
             "id",
             "client",
             "user",
             "workspaces",
             "access",
             "surface",
             "paused",
             "createdAt",
             "lastUsedAt",
             "expiresAt"
            ],
            "properties": {
             "id": {
              "type": "string",
              "format": "uuid"
             },
             "client": {
              "type": "object",
              "required": [
               "name",
               "redirectHost",
               "kind"
              ],
              "properties": {
               "name": {
                "type": "string",
                "description": "The application's name, as its registration claims it."
               },
               "redirectHost": {
                "type": "string",
                "description": "Where its code would go: a host, or an app's callback."
               },
               "kind": {
                "type": "string",
                "enum": [
                 "web",
                 "loopback",
                 "app"
                ]
               }
              }
             },
             "user": {
              "type": "object",
              "required": [
               "id",
               "name"
              ],
              "properties": {
               "id": {
                "type": "string"
               },
               "name": {
                "type": "string"
               }
              }
             },
             "workspaces": {
              "type": "array",
              "items": {
               "type": "object",
               "required": [
                "id",
                "name",
                "default"
               ],
               "properties": {
                "id": {
                 "type": "string",
                 "description": "Your workspace id; `default` for the account's default workspace."
                },
                "name": {
                 "type": "string"
                },
                "default": {
                 "type": "boolean"
                }
               }
              }
             },
             "access": {
              "type": "string",
              "enum": [
               "read",
               "run",
               "draft",
               "publish"
              ]
             },
             "surface": {
              "type": "string",
              "enum": [
               "build",
               "tools"
              ]
             },
             "paused": {
              "type": [
               "string",
               "null"
              ],
              "description": "The first switch off that this connection needs (it works again when the switch is back on), or null."
             },
             "createdAt": {
              "type": "string",
              "format": "date-time"
             },
             "lastUsedAt": {
              "type": [
               "string",
               "null"
              ],
              "format": "date-time"
             },
             "expiresAt": {
              "type": "string",
              "format": "date-time"
             }
            }
           }
          },
          "next": {
           "type": [
            "string",
            "null"
           ],
           "description": "The same cursor as `pagination.nextCursor`."
          },
          "pagination": {
           "$ref": "#/components/schemas/EmbedCursorPagination"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `limit`, `cursor` or `user`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — the partner has no account with this id.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/ai-apps/{grantId}": {
   "delete": {
    "operationId": "embedRevokeAIApp",
    "summary": "Disconnect one AI app",
    "description": "Ends one connection at once: its tokens stop working and the app must sign in again. Audited `oauth.consent.revoked` with reason `partner`. One already ended answers 200 as well; another account's connection is a 404.",
    "tags": [
     "Embed: AI apps"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "grantId",
      "in": "path",
      "required": true,
      "description": "The connection's id, as the list answered it.",
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "Disconnected.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "id",
            "revoked"
           ],
           "properties": {
            "id": {
             "type": "string",
             "format": "uuid"
            },
            "revoked": {
             "type": "boolean"
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such connection in this account.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/ai-apps/revoke": {
   "post": {
    "operationId": "embedRevokeAccountAIApps",
    "summary": "Disconnect an account's AI apps",
    "description": "Ends every AI app connected to the account, or one person's only with `user`. Answers how many. Idempotent with an `Idempotency-Key`.",
    "tags": [
     "Embed: AI apps"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "description": "Retry safely: the same key (per embed API key) with the same request answers the first response again, with `Idempotency-Replayed: true`, for 24 hours; only a success is kept, so a retry after a failure is a new try. The same key with a different request is 422 `VALIDATION_ERROR`, one over 255 characters is 400 `VALIDATION_ERROR`, and 409 `CONFLICT` says a request with this key is still running: retry shortly."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "user": {
          "type": [
           "string",
           "null"
          ],
          "description": "Only this person's (your user id)."
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "`revoked` counts the connections ended.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "revoked"
           ],
           "properties": {
            "revoked": {
             "type": "integer",
             "description": "How many connections this call ended."
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `user` is not a valid id, or the body is not a JSON object. `VALIDATION_ERROR` — also an `Idempotency-Key` over 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — the partner has no account with this id.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `CONFLICT` — a request with this `Idempotency-Key` is still running; retry shortly.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "code `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/ai-apps/revoke": {
   "post": {
    "operationId": "embedRevokeAllAIApps",
    "summary": "Disconnect every AI app on the installation",
    "description": "The incident tool: ends every AI app that signed in through your product, in every account. People must sign in again to connect them. Turning a switch off only pauses connections; this ends them. Answers how many. Audited `embed.ai_apps.revoke_all` with the count, plus one `oauth.consent.revoked` per connection. Idempotent with an `Idempotency-Key`. After a leaked key, revoke the KEY instead (Platform › Partner keys): that ends exactly the connections whose latest sign-in it made, and a rotation alone keeps them.",
    "tags": [
     "Embed: AI apps"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "description": "Retry safely: the same key (per embed API key) with the same request answers the first response again, with `Idempotency-Replayed: true`, for 24 hours; only a success is kept, so a retry after a failure is a new try. The same key with a different request is 422 `VALIDATION_ERROR`, one over 255 characters is 400 `VALIDATION_ERROR`, and 409 `CONFLICT` says a request with this key is still running: retry shortly."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "`revoked` counts the connections ended.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "revoked"
           ],
           "properties": {
            "revoked": {
             "type": "integer",
             "description": "How many connections this call ended."
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `user` is per account (sent here, it is refused), or the body is not a JSON object. `VALIDATION_ERROR` — also an `Idempotency-Key` over 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `CONFLICT` — a request with this `Idempotency-Key` is still running; retry shortly.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "code `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the key's window (`X-RateLimit-Scope: key`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/tokens": {
   "get": {
    "operationId": "embedListTokens",
    "summary": "List an account's tokens",
    "description": "The account's live API tokens (not revoked, not expired), newest first: the ones its editors made on the Tokens page and the ones your server made. Never the token itself.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workspaceId",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      },
      "description": "Only this workspace's tokens (the partner's workspace id). Omit for every workspace. `default` names the account's default workspace."
     },
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "The `pagination.nextCursor` of the previous page. Omit for the first page."
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 50
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "A page of tokens.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data",
          "pagination"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/EmbedToken"
           }
          },
          "pagination": {
           "$ref": "#/components/schemas/EmbedCursorPagination"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — the partner has no account with this id, or the account has no workspace with `workspaceId`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   },
   "post": {
    "operationId": "embedCreateToken",
    "summary": "Make a token for an account",
    "description": "Makes an API token (`grt_…`) that works in one of the account's workspaces, for your own automation or to hand to the customer: on the App API and, with `surfaces` holding `mcp`, with an AI app at `{your installation}/mcp/tools` (and `/mcp/build` while MCP building is on). It can do no more than an editor of the account, never reaches another account, and is judged on every request: it stops while the Tokens switch is off for the account, reads only while the account is suspended, and is revoked when the account is deleted or the key that made it is revoked (a rotation does not stop it). Every token expires: `expiresInDays` from 7 to 365, 90 when omitted. A workspace holds at most 25 live tokens. `raw` is the token, answered this once and never again. Not idempotent: an Idempotency-Key is not taken, because a stored replay would keep the token for a day; a retry makes a second token, which you can list and revoke.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/EmbedTokenInput"
       }
      }
     }
    },
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "201": {
      "description": "Made. `raw` is shown once.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "token",
            "raw"
           ],
           "properties": {
            "token": {
             "$ref": "#/components/schemas/EmbedToken"
            },
            "raw": {
             "type": "string",
             "description": "The token, `grt_` and 40 hex characters. Shown once."
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller. `FEATURE_OFF` — the Tokens switch is off for this account (`error.details.switch` is `tokens`), or `surfaces` holds `mcp` while MCP is off (`mcp`); `FORBIDDEN` — the key that made the request can no longer make tokens. A `FORBIDDEN` for a scope names it in `error.details.scope`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — the partner has no account with this id, or the account has no workspace with `workspaceId`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted; restore it first. `TOKEN_LIMIT` — the workspace already has 25 live tokens; revoke one first.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/tokens/{tokenId}": {
   "delete": {
    "operationId": "embedRevokeToken",
    "summary": "Revoke a token",
    "description": "Revokes any of the account's tokens, one its editors made or one your server made. Apps using it stop working at once. Revoking a token already revoked answers 204 as well.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "tokenId",
      "in": "path",
      "required": true,
      "description": "The token's id, as `token.id` answered it.",
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "204": {
      "description": "Revoked."
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — the partner has no account with this id, or the account has no token with this id.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/websites": {
   "get": {
    "operationId": "embedListAccountWebsites",
    "summary": "List one customer's own websites",
    "description": "Websites that show only this account's sessions (a customer's own domain), up to `max` (20).",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The list, alphabetically.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedAccountWebsites"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tightest window and `X-RateLimit-Scope` names it (`key`, `account`, `person` or `session`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   },
   "put": {
    "operationId": "embedPutAccountWebsites",
    "summary": "Replace one customer's own websites",
    "description": "The whole list: exact origins only (no wildcard), duplicates dropped, at most 20; an empty list removes them all. A website kept keeps the day it was added. Sessions of this account open on a website taken off end (`website_removed`), unless the installation's own list allows it.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "websites"
        ],
        "properties": {
         "websites": {
          "type": "array",
          "maxItems": 20,
          "items": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    },
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The list now.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedAccountWebsites"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `websites` is missing, the body is not a JSON object, or a field has the wrong type (`error.details.field`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "code `INVALID_ORIGIN` — the website can't be allowed as written, or is a wildcard: a customer's website is one exact address (`details.invalid` lists every problem of a list); `TOO_MANY_WEBSITES` — more than 20 (`details.max`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tightest window and `X-RateLimit-Scope` names it (`key`, `account`, `person` or `session`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   },
   "post": {
    "operationId": "embedAddAccountWebsite",
    "summary": "Allow a website for one customer",
    "description": "One exact website (no wildcard) that shows only this account's sessions: a login code of another account is refused there (`ORIGIN_NOT_ALLOWED`). No two-factor code; recorded in the account's audit log as the key.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "description": "Retry safely: the same key (per embed API key) with the same request answers the first response again, with `Idempotency-Replayed: true`, for 24 hours; only a success is kept, so a retry after a failure is a new try. The same key with a different request is 422 `VALIDATION_ERROR`, one over 255 characters is 400 `VALIDATION_ERROR`, and 409 `CONFLICT` says a request with this key is still running: retry shortly."
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "origin"
        ],
        "properties": {
         "origin": {
          "type": "string",
          "description": "An origin: `https://` and a host, an optional port, nothing after it."
         }
        }
       }
      }
     }
    },
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "201": {
      "description": "Added.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedAccountWebsite"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`), or the body is not a JSON object, or `origin` is not text (`error.details.field`). `VALIDATION_ERROR` — also an `Idempotency-Key` over 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `WEBSITE_EXISTS` — the list already holds it; `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first. `CONFLICT` — a request with this `Idempotency-Key` is still running; retry shortly.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "code `INVALID_ORIGIN` — the website can't be allowed as written, or is a wildcard: a customer's website is one exact address (`details.invalid` lists every problem of a list); `TOO_MANY_WEBSITES` — more than 20 (`details.max`). `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request. A body with no `origin` is `INVALID_ORIGIN` too.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tightest window and `X-RateLimit-Scope` names it (`key`, `account`, `person` or `session`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/websites/{origin}": {
   "delete": {
    "operationId": "embedRemoveAccountWebsite",
    "summary": "Stop allowing a website for one customer",
    "description": "The account's sessions open on it end (`website_removed`) unless the installation's own list allows it.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "origin",
      "in": "path",
      "required": true,
      "description": "The website, URL-escaped: `https%3A%2F%2Fapp.example.com`.",
      "schema": {
       "type": "string",
       "minLength": 1
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "204": {
      "description": "Removed."
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, or no such website on its list.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tightest window and `X-RateLimit-Scope` names it (`key`, `account`, `person` or `session`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/workspaces": {
   "get": {
    "operationId": "embedListWorkspaces",
    "summary": "List an account's workspaces",
    "description": "The default workspace first.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "The `pagination.nextCursor` of the previous page. Omit for the first page."
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 50
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "A page of workspaces.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data",
          "pagination"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/EmbedWorkspace"
           }
          },
          "pagination": {
           "$ref": "#/components/schemas/EmbedCursorPagination"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — the partner has no account with this id.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/workspaces/{workspaceId}": {
   "put": {
    "operationId": "embedPutWorkspace",
    "summary": "Create or rename a workspace",
    "description": "`name` is required to create; a bare body on an existing workspace changes nothing. A name another workspace of the account already has is suffixed with the partner's id. Writes go through in a suspended account.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workspaceId",
      "in": "path",
      "required": true,
      "description": "The partner's workspace id (same rules as account ids). `default` names the account's default workspace, which has no id of its own: `PUT …/workspaces/default` renames it (it never makes a second workspace), and it is answered with `id` null and `default` true. (An account that already holds a workspace you named `default` before v2026.10.8 keeps that one under this id.)",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 200
         }
        }
       }
      }
     }
    },
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "Renamed.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedWorkspace"
          }
         }
        }
       }
      }
     },
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedWorkspace"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller; `WORKSPACE_LIMIT` — creating the workspace would pass the license's workspace limit.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — the partner has no account with this id.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first. `BUSY` — many accounts or workspaces were being created at once under the license's limit, and this one gave up waiting for its turn: nothing was done; try again in a moment.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   },
   "get": {
    "operationId": "embedGetWorkspace",
    "summary": "Get a workspace",
    "description": "",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workspaceId",
      "in": "path",
      "required": true,
      "description": "The partner's workspace id (same rules as account ids). `default` names the account's default workspace, which has no id of its own: `PUT …/workspaces/default` renames it (it never makes a second workspace), and it is answered with `id` null and `default` true. (An account that already holds a workspace you named `default` before v2026.10.8 keeps that one under this id.)",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The workspace.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedWorkspace"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — an id in the path (`accountId`, `workspaceId`) is not valid or not escaped correctly; `error.details.field` names it (`account.id`, `workspace.id`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   },
   "delete": {
    "operationId": "embedDeleteWorkspace",
    "summary": "Delete a workspace, permanently",
    "description": "Its run payloads, then everything in it. Sessions in it end (`workspace_deleted`).",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workspaceId",
      "in": "path",
      "required": true,
      "description": "The partner's workspace id (same rules as account ids). `default` names the account's default workspace, which has no id of its own: `PUT …/workspaces/default` renames it (it never makes a second workspace), and it is answered with `id` null and `default` true. (An account that already holds a workspace you named `default` before v2026.10.8 keeps that one under this id.)",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "204": {
      "description": "Deleted."
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — an id in the path (`accountId`, `workspaceId`) is not valid or not escaped correctly; `error.details.field` names it (`account.id`, `workspace.id`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first. `DEFAULT_WORKSPACE` — an account's default workspace can't be deleted. `BUSY` — something else held the workspace (a flush of one of its runs, another delete) and the delete gave up waiting for it after 30 s; nothing was deleted, try again in a moment.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/members": {
   "get": {
    "operationId": "embedListMembers",
    "summary": "List an account's members",
    "description": "One row per person per workspace, oldest first.",
    "tags": [
     "Embed: people"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workspaceId",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      },
      "description": "Narrow to one workspace (the partner's id). Omitted = every workspace; the empty value is refused. `default` names the account's default workspace."
     },
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "The `pagination.nextCursor` of the previous page. Omit for the first page."
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 50
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "A page of memberships.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data",
          "pagination"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/EmbedMember"
           }
          },
          "pagination": {
           "$ref": "#/components/schemas/EmbedCursorPagination"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account or workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/members/{userId}": {
   "put": {
    "operationId": "embedPutMember",
    "summary": "Give a person a role in a workspace",
    "description": "`workspaceId` omitted = the default workspace. An unknown person is created when `user.name` is sent. A changed role ends the person's older sessions there (`role_changed`).",
    "tags": [
     "Embed: people"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "userId",
      "in": "path",
      "required": true,
      "description": "The partner's user id (same rules as account ids).",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/EmbedMemberInput"
       }
      }
     }
    },
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "Updated.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedMember"
          }
         }
        }
       }
      }
     },
     "201": {
      "description": "The membership is new.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedMember"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, workspace or person (send `user.name` to create them).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   },
   "delete": {
    "operationId": "embedRemoveMember",
    "summary": "Remove a person from a workspace or the account",
    "description": "With `workspaceId`: that workspace only (and the account too when it was their last there). Without: every workspace of the account. Their sessions there end (`access_removed`).",
    "tags": [
     "Embed: people"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "userId",
      "in": "path",
      "required": true,
      "description": "The partner's user id (same rules as account ids).",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workspaceId",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      },
      "description": "Narrow to one workspace (the partner's id). Omitted = every workspace; the empty value is refused. `default` names the account's default workspace."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "204": {
      "description": "Removed."
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/users/{userId}": {
   "get": {
    "operationId": "embedGetUser",
    "summary": "Get a person",
    "description": "With every workspace they hold a role in.",
    "tags": [
     "Embed: people"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "userId",
      "in": "path",
      "required": true,
      "description": "The partner's user id (same rules as account ids).",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The person.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedUser"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `userId` in the path is not valid or not escaped correctly (`error.details.field`: `user.id`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the key's window (`X-RateLimit-Scope: key`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   },
   "put": {
    "operationId": "embedPutUser",
    "summary": "Create or update a person",
    "description": "`name` is required to create. `email` is contact data only — never identity, never a sign-in; null clears it.",
    "tags": [
     "Embed: people"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "userId",
      "in": "path",
      "required": true,
      "description": "The partner's user id (same rules as account ids).",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/EmbedUserInput"
       }
      }
     }
    },
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "Updated.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedUser"
          }
         }
        }
       }
      }
     },
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedUser"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the key's window (`X-RateLimit-Scope: key`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   },
   "delete": {
    "operationId": "embedDeleteUser",
    "summary": "Delete a person everywhere",
    "description": "Every membership, their assistant conversations and what identifies them; their sessions end. The id is released: the same id names a new person next time.",
    "tags": [
     "Embed: people"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "userId",
      "in": "path",
      "required": true,
      "description": "The partner's user id (same rules as account ids).",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "204": {
      "description": "Deleted."
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `userId` in the path is not valid or not escaped correctly (`error.details.field`: `user.id`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the key's window (`X-RateLimit-Scope: key`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/websites": {
   "get": {
    "operationId": "embedListWebsites",
    "summary": "List the installation's allowed websites",
    "description": "The websites that may show any customer's sessions (Platform › Partner keys › Allowed websites), up to `max` (200). The same list the console edits; the console asks for a two-factor code, the key does not.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The list.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedWebsites"
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the key's window (`X-RateLimit-Scope: key`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   },
   "post": {
    "operationId": "embedAddWebsite",
    "summary": "Allow a website for every customer",
    "description": "Adds one website to the installation's list, normalized as the console does (`https://*.example.com` covers every subdomain of a domain you own). It applies to the next load of the embed. For a website that belongs to one customer, use `POST /accounts/{accountId}/websites` instead.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "description": "Retry safely: the same key (per embed API key) with the same request answers the first response again, with `Idempotency-Replayed: true`, for 24 hours; only a success is kept, so a retry after a failure is a new try. The same key with a different request is 422 `VALIDATION_ERROR`, one over 255 characters is 400 `VALIDATION_ERROR`, and 409 `CONFLICT` says a request with this key is still running: retry shortly."
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "origin"
        ],
        "properties": {
         "origin": {
          "type": "string",
          "description": "An origin: `https://` and a host, an optional port, nothing after it."
         }
        }
       }
      }
     }
    },
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "201": {
      "description": "Added; `origin` as stored.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "origin"
           ],
           "properties": {
            "origin": {
             "type": "string"
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the body is not a JSON object, or `origin` is not text (`error.details.field`). `VALIDATION_ERROR` — also an `Idempotency-Key` over 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `WEBSITE_EXISTS` — the list already holds it. `CONFLICT` — a request with this `Idempotency-Key` is still running; retry shortly.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "code `INVALID_ORIGIN` — the website can't be allowed as written (the message says why; `details.field`, `details.value`); `TOO_MANY_WEBSITES` — the list already holds 200 (`details.max`). `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request. A body with no `origin` is `INVALID_ORIGIN` too.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the key's window (`X-RateLimit-Scope: key`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/websites/{origin}": {
   "delete": {
    "operationId": "embedRemoveWebsite",
    "summary": "Stop allowing a website for every customer",
    "description": "The next load of the embed there is refused; a session already open carries on until it would renew.",
    "tags": [
     "Embed: accounts"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "origin",
      "in": "path",
      "required": true,
      "description": "The website, URL-escaped: `https%3A%2F%2Fapp.example.com`.",
      "schema": {
       "type": "string",
       "minLength": 1
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "204": {
      "description": "Removed."
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such website on this list.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the key's window (`X-RateLimit-Scope: key`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/templates": {
   "get": {
    "operationId": "embedListTemplates",
    "summary": "List the templates customers can install",
    "description": "The installation's published templates (shared with every customer from a workflow's ⋯ in Workflows: Share with customers…), newest first, with every app each one uses. `key` is the stable handle `POST /accounts/{accountId}/workflows` installs by; `connections` lists the connections a workflow made from it needs, one slot per service. An account whose app list leaves out one of a template's apps cannot install it.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "The `pagination.nextCursor` of the previous page. Omit for the first page."
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 50
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "A page of templates.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data",
          "pagination"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/EmbedTemplate"
           }
          },
          "pagination": {
           "$ref": "#/components/schemas/EmbedCursorPagination"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the key's window (`X-RateLimit-Scope: key`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/connections": {
   "get": {
    "operationId": "embedListConnections",
    "summary": "List an account's connections",
    "description": "Every connection in the account, newest first: the ones the partner manages (`managedBy: partner`, with the partner's `key`) and the ones the customer made in the embedded product (`managedBy: customer`, `key` null). Never a secret.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workspaceId",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      },
      "description": "Narrow to one workspace (the partner's id). Omitted = every workspace; the empty value is refused. `default` names the account's default workspace."
     },
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "The `pagination.nextCursor` of the previous page. Omit for the first page."
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 50
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "A page of connections.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data",
          "pagination"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/EmbedConnection"
           }
          },
          "pagination": {
           "$ref": "#/components/schemas/EmbedCursorPagination"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account or workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/connections/{connectionKey}": {
   "get": {
    "operationId": "embedGetConnection",
    "summary": "Get a managed connection",
    "description": "By the partner's key. Never a secret.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "connectionKey",
      "in": "path",
      "required": true,
      "description": "The partner's key for the connection (same rules as account ids), unique within one workspace. URL-escape it.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workspaceId",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      },
      "description": "The workspace the key is in (the partner's id). Omitted = the account's default workspace; the empty value is refused. `default` names the account's default workspace."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The connection.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedConnection"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, workspace or connection key.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   },
   "put": {
    "operationId": "embedPutConnection",
    "summary": "Create or replace a managed connection",
    "description": "Stores a credential the partner manages for its customer, by the partner's key: created the first time, replaced after (the app cannot change — delete the key first). The customer's workflows can use it; the customer cannot edit, re-authorise or delete it. `values` holds the app's auth fields and is write-only: no response ever carries it. For an OAuth2 app that is `accessToken` (required), `refreshToken`, `expiresAt` (RFC 3339) and `tokenType`, plus `clientId` and `clientSecret` when the tokens came from the partner's own app; `oauthApp: \"installation\"` instead records that they came from the installation's app for that service (Platform › OAuth apps), so they refresh through it. `authType` is optional and must be one the app declares. Every field is checked before anything is written: a key the app does not have is refused by name (`details.field` `values.<key>`, `details.unknown`, the app's own `details.fields`), each required field must be given, and an app whose fields are all optional needs at least one.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "connectionKey",
      "in": "path",
      "required": true,
      "description": "The partner's key for the connection (same rules as account ids), unique within one workspace. URL-escape it.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/EmbedConnectionInput"
       }
      }
     }
    },
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "Replaced.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedConnection"
          }
         }
        }
       }
      }
     },
     "201": {
      "description": "Created.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedConnection"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field: an unknown app, a missing or malformed auth value, an `authType` the app does not declare, or `oauthApp` for a service the installation has no app for. Also an app that takes no connection (`details.field` `pieceId`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller; `PIECE_NOT_ALLOWED` — this account may not use the app.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account or workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED`; `CONFLICT` — the key already names a connection on another app.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "503": {
      "description": "code `PIECES_UNAVAILABLE` — which apps the account may use could not be read; nothing was written. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   },
   "delete": {
    "operationId": "embedDeleteConnection",
    "summary": "Delete a managed connection",
    "description": "Refused while a live workflow still uses it: disable or change those first.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "connectionKey",
      "in": "path",
      "required": true,
      "description": "The partner's key for the connection (same rules as account ids), unique within one workspace. URL-escape it.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workspaceId",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      },
      "description": "The workspace the key is in (the partner's id). Omitted = the account's default workspace; the empty value is refused. `default` names the account's default workspace."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "204": {
      "description": "Deleted."
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, workspace or connection key.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `CONFLICT` — a live workflow still uses the connection (`error.details.workflows` lists their ids); `ACCOUNT_DELETED`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/connections/oauth2/start": {
   "post": {
    "operationId": "embedStartConnectionSignIn",
    "summary": "Start a connection's OAuth sign-in for your own Connect button",
    "description": "Your server starts the OAuth sign-in of one of the account's connections, and your page sends your customer's browser straight to the provider: no GoRunner screen. The answer's `authorizeUrl` is the provider's consent page (Google's, for Gmail), with the installation's app (Platform › OAuth apps) unless `config` names your own. When the provider answers, the callback creates the connection in the account's workspace exactly as one made in the embed — the customer's, `managedBy: customer`, its tokens encrypted and refreshing, `connection.create` audited naming this key, and your `connection.created` event — then redirects (302) to `returnUrl` with `status=connected&connectionId=…&attemptId=…`, or `status=error&reason=…&attemptId=…`. Never a token or a secret in that address. Use `connectionId` in a workflow step's `connectionId` (POST `/api/workflows` with a build token), and read the outcome with `GET …/connections/oauth2/attempts/{attemptId}`.\n\n`returnUrl` must be an absolute https address (http only for localhost) on EXACTLY one of the installation's allowed websites or one of this account's own (`/accounts/{accountId}/websites`); a wildcard entry allows none. Anything else is refused before anything is stored. The link is single-use, works for 10 minutes and is bound on the server to this key, this account's workspace and the app. Sign an existing connection in again with `connectionId` (not a partner-managed one: replace those with `PUT …/connections/{connectionKey}`).\n\nNot idempotency-wrapped on purpose: a stored replay would hand back a link already used or run out. A retry starts a new sign-in; the unused one expires.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/EmbedConnectionSignInInput"
       },
       "example": {
        "pieceId": "gmail",
        "returnUrl": "https://app.partner.example/integrations/google/done?user=u_123"
       }
      }
     }
    },
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "Started.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/ConnectionOAuth2APIStartResult"
          }
         }
        },
        "example": {
         "data": {
          "attemptId": "Zq3XbN0iJm7dQe8TfR2wLg",
          "authorizeUrl": "https://accounts.google.com/o/oauth2/v2/auth?…&state=…",
          "expiresAt": "2026-10-21T12:10:00Z"
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field (`pieceId`, `name`, `connectionId`, `returnUrl`, `mode`, `workspaceId`); the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller; `PIECE_NOT_ALLOWED` — this account may not use the app. `MANAGED_CONNECTION` — `connectionId` names a partner-managed connection.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, workspace, or (`connectionId`) connection in that workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account was deleted; nothing is started in it.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "code `RETURN_URL_NOT_ALLOWED` — `returnUrl`'s origin is not an allowed website (`details.origin`); `VALIDATION_ERROR` — no app to sign in with for the service.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "503": {
      "description": "code `UNAVAILABLE` — sign-ins are not available on this server, or the allowed websites could not be read; nothing was started.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/connections/oauth2/attempts/{attemptId}": {
   "get": {
    "operationId": "embedGetConnectionSignIn",
    "summary": "Read how a connection's sign-in ended",
    "description": "A sign-in this key started for this account: `pending` until the provider sends the browser back, then `connected` (with `connectionId`) or `failed` (with `reason`). Another key's, another account's or a person's sign-in is not found here. A sign-in is kept for an hour after its link runs out.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "attemptId",
      "in": "path",
      "required": true,
      "description": "The `attemptId` the start answered.",
      "schema": {
       "type": "string"
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "Where the sign-in stands.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/ConnectionOAuth2APIAttempt"
          }
         }
        },
        "example": {
         "data": {
          "attemptId": "Zq3XbN0iJm7dQe8TfR2wLg",
          "status": "connected",
          "connectionId": "0f8b2c7e-4a51-4d3e-9b2a-6c1d2e3f4a5b",
          "account": "ada@bakery.example",
          "expiresAt": "2026-10-21T12:10:00Z"
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, or no sign-in with that id started by this key for it.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "503": {
      "description": "code `UNAVAILABLE` — signing in to services isn't available on this server right now (no Redis).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/workflows": {
   "get": {
    "operationId": "embedListWorkflows",
    "summary": "List an account's workflows",
    "description": "Newest first. Never a definition.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workspaceId",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      },
      "description": "Narrow to one workspace (the partner's id). Omitted = every workspace; the empty value is refused. `default` names the account's default workspace."
     },
     {
      "name": "status",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "draft",
        "live",
        "paused"
       ]
      }
     },
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "The `pagination.nextCursor` of the previous page. Omit for the first page."
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 50
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "A page of workflows.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data",
          "pagination"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/EmbedWorkflow"
           }
          },
          "pagination": {
           "$ref": "#/components/schemas/EmbedCursorPagination"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account or workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   },
   "post": {
    "operationId": "embedInstallWorkflow",
    "summary": "Install a template as a workflow",
    "description": "Makes a workflow in the account from a published template, by its `key`, and wires each of the template's connection slots to one of the account's connections: `connections` maps a slot's `key` to `{\"key\": \"<the partner's connection key>\"}` or `{\"id\": \"<connection id>\"}` (one the customer made). A slot left out stays empty and is listed in `install.missing`. With `enable: true` the workflow is also published so its trigger goes live; when it cannot be, it is still made, as a draft, and `install.publishRefusal` says why. The answer is 201 either way, so a retry with the same `Idempotency-Key` replays it.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "description": "Retry safely: the same key (per embed API key) with the same request answers the first response again, with `Idempotency-Replayed: true`, for 24 hours; only a success is kept, so a retry after a failure is a new try. The same key with a different request is 422 `VALIDATION_ERROR`, one over 255 characters is 400 `VALIDATION_ERROR`, and 409 `CONFLICT` says a request with this key is still running: retry shortly."
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/EmbedWorkflowInstallInput"
       }
      }
     }
    },
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "201": {
      "description": "The workflow, and how the install went.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedInstalledWorkflow"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal. `VALIDATION_ERROR` — also an `Idempotency-Key` over 255 characters. Also a slot the template does not have (`error.details.field` `connections`, `error.details.slot`), and a template holding a condition operator the engine does not implement.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, workspace or published template; or a slot's connection (by `key` or `id`) is not in the workspace, and `error.details.slot` names the slot. Nothing was made.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first. `CONFLICT` — a request with this `Idempotency-Key` is still running; retry shortly.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "Nothing was made. Code `CONNECTION_MISMATCH` — a connection is for another service than its slot needs; `error.details.slot` names the slot. `PIECE_NOT_ALLOWED` — the template uses an app this account may not use. `VALIDATION_ERROR` — the template uses an app this installation's workers don't run. `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "503": {
      "description": "code `PIECES_UNAVAILABLE` — which apps the account may use could not be read; nothing was written. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/workflows/{workflowId}": {
   "get": {
    "operationId": "embedGetWorkflow",
    "summary": "Get a workflow",
    "description": "Never its definition.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workflowId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid"
      },
      "description": "The workflow's id, as the install or the list answered it."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The workflow.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedWorkflow"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, or no such workflow in it.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/workflows/{workflowId}/enable": {
   "post": {
    "operationId": "embedEnableWorkflow",
    "summary": "Enable a workflow",
    "description": "Publishes the workflow's current draft so its trigger is live. Enabling a live workflow whose draft has not changed changes nothing.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workflowId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid"
      },
      "description": "The workflow's id, as the install or the list answered it."
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "description": "Retry safely: the same key (per embed API key) with the same request answers the first response again, with `Idempotency-Replayed: true`, for 24 hours; only a success is kept, so a retry after a failure is a new try. The same key with a different request is 422 `VALIDATION_ERROR`, one over 255 characters is 400 `VALIDATION_ERROR`, and 409 `CONFLICT` says a request with this key is still running: retry shortly."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The workflow, live.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedWorkflow"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`). `VALIDATION_ERROR` — also an `Idempotency-Key` over 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, or no such workflow in it.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first. `CONFLICT` — another change to this workflow is being saved; try again in a moment. `CONFLICT` — a request with this `Idempotency-Key` is still running; retry shortly.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "The workflow cannot go live; nothing changed. Code `VALIDATION_ERROR`, the message naming the first fault (for example a step without its connection), or `PIECE_NOT_ALLOWED` — the same `{code, message}` an install reports in `install.publishRefusal`. `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "503": {
      "description": "code `PIECES_UNAVAILABLE` — which apps the account may use could not be read; nothing was written. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/workflows/{workflowId}/disable": {
   "post": {
    "operationId": "embedDisableWorkflow",
    "summary": "Disable a workflow",
    "description": "Turns the workflow's trigger off. It keeps its published version and can be enabled again. Disabling a workflow that is not live changes nothing.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workflowId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid"
      },
      "description": "The workflow's id, as the install or the list answered it."
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "description": "Retry safely: the same key (per embed API key) with the same request answers the first response again, with `Idempotency-Replayed: true`, for 24 hours; only a success is kept, so a retry after a failure is a new try. The same key with a different request is 422 `VALIDATION_ERROR`, one over 255 characters is 400 `VALIDATION_ERROR`, and 409 `CONFLICT` says a request with this key is still running: retry shortly."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The workflow.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedWorkflow"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`). `VALIDATION_ERROR` — also an `Idempotency-Key` over 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, or no such workflow in it.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first. `CONFLICT` — another change to this workflow is being saved; try again in a moment. `CONFLICT` — a request with this `Idempotency-Key` is still running; retry shortly.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "code `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/runs": {
   "get": {
    "operationId": "embedListRuns",
    "summary": "List an account's runs",
    "description": "Production runs, newest first. A run a sub-flow step started is part of the run that started it and is not listed on its own. Never step data: `error` is the failure's sentence only.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workspaceId",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      },
      "description": "Narrow to one workspace (the partner's id). Omitted = every workspace; the empty value is refused. `default` names the account's default workspace."
     },
     {
      "name": "workflowId",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "format": "uuid"
      },
      "description": "Only this workflow's runs."
     },
     {
      "name": "status",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "queued",
        "running",
        "held",
        "worked",
        "failed",
        "cancelled"
       ]
      },
      "description": "Only runs in this state, as the run reads it: `queued` (no worker has it yet), `running` (a worker has it), and so on."
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "format": "date-time"
      },
      "description": "Runs that started at or after this instant."
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "format": "date-time"
      },
      "description": "Runs that started before this instant."
     },
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "The `pagination.nextCursor` of the previous page. Omit for the first page."
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 50
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "A page of runs.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data",
          "pagination"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/EmbedRun"
           }
          },
          "pagination": {
           "$ref": "#/components/schemas/EmbedCursorPagination"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account or workspace.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/runs/{runId}": {
   "get": {
    "operationId": "embedGetRun",
    "summary": "Get a run",
    "description": "Never step data.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "runId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid"
      },
      "description": "The run's id."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The run.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedRun"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, or no such run in it.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/runs/{runId}/cancel": {
   "post": {
    "operationId": "embedCancelRun",
    "summary": "Cancel a run",
    "description": "Stops one of the account's runs, exactly as Cancel on the Runs page: a run waiting for a free slot leaves the line and never runs, a held run (waiting on a person or a timer) is settled, a running one is asked to stop: this answer says `cancelling`, a read of the run keeps saying `running` until its worker has stopped, and then `cancelled`. A run that already finished is left as it is: `cancelled: false` with its status. Cancelling works in a suspended account. Only the runs this API lists (production, started by a trigger) are reachable. Leaves the account's trail an `embed.run.cancel` row.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "runId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid"
      },
      "description": "The run's id."
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "description": "Retry safely: the same key (per embed API key) with the same request answers the first response again, with `Idempotency-Replayed: true`, for 24 hours; only a success is kept, so a retry after a failure is a new try. The same key with a different request is 422 `VALIDATION_ERROR`, one over 255 characters is 400 `VALIDATION_ERROR`, and 409 `CONFLICT` says a request with this key is still running: retry shortly."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The run as it now stands.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "run",
            "cancelled"
           ],
           "properties": {
            "run": {
             "type": "object",
             "required": [
              "id",
              "status"
             ],
             "properties": {
              "id": {
               "type": "string",
               "format": "uuid"
              },
              "status": {
               "type": "string",
               "description": "The run's status now, as EmbedRun says it, or `cancelling` while a running run stops."
              }
             }
            },
            "cancelled": {
             "type": "boolean",
             "description": "Whether this call stopped it; false for a run that had already finished."
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`). `VALIDATION_ERROR` — also an `Idempotency-Key` over 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, or no such run in it.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first. `CONFLICT` — a request with this `Idempotency-Key` is still running; retry shortly.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "code `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/runs/{runId}/rerun": {
   "post": {
    "operationId": "embedRerunRun",
    "summary": "Run a run again",
    "description": "Starts the run again, exactly as Run again on the Runs page: a new production run of the workflow's published version, with the trigger data the run started with (headers, query and body as they arrived), recorded as a retry of it (`retryOf`). Over a runs-at-once limit the new run waits in line. Leaves the account's trail an `embed.run.rerun` row.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "runId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid"
      },
      "description": "The run's id."
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "description": "Retry safely: the same key (per embed API key) with the same request answers the first response again, with `Idempotency-Replayed: true`, for 24 hours; only a success is kept, so a retry after a failure is a new try. The same key with a different request is 422 `VALIDATION_ERROR`, one over 255 characters is 400 `VALIDATION_ERROR`, and 409 `CONFLICT` says a request with this key is still running: retry shortly."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "201": {
      "description": "The new run.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "run"
           ],
           "properties": {
            "run": {
             "allOf": [
              {
               "$ref": "#/components/schemas/EmbedRun"
              },
              {
               "type": "object",
               "required": [
                "retryOf"
               ],
               "properties": {
                "retryOf": {
                 "type": "string",
                 "format": "uuid",
                 "description": "The run this one runs again."
                }
               }
              }
             ]
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`). `VALIDATION_ERROR` — also an `Idempotency-Key` over 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, no such run in it, or the run's workflow was deleted.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `WORKFLOW_NOT_PUBLISHED` — the workflow is not on, so it has nothing published to run; `ACCOUNT_SUSPENDED` — the account is suspended (resume it first); `ACCOUNT_DELETED` — the account is deleted. `CONFLICT` — a request with this `Idempotency-Key` is still running; retry shortly.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "code `PIECE_NOT_ALLOWED` or `VALIDATION_ERROR` — the published version has a step the account may not use, or cannot run; nothing was started. `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows. Or, from the run it starts (the same envelope as `POST /api/workflows/{id}/run`): `RUNS_WAITING_LIMIT` — the account already has its cap of runs waiting (\"Too many runs are waiting for this organization. Try again later.\") or the workflow has its own (\"Too many runs of this workflow are waiting. Try again later.\"); `CONCURRENCY_LIMIT` — the workflow is at its runs-at-once limit and chose `settings.whenFull: refuse`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "503": {
      "description": "code `PIECES_UNAVAILABLE` — which apps the account may use could not be read; nothing was started. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/workflows/{workflowId}/cancel-waiting": {
   "post": {
    "operationId": "embedCancelWaitingRuns",
    "summary": "Cancel every waiting run of a workflow",
    "description": "Takes every run of one of the account's workflows that is waiting in the line for a free slot out of it, in one transaction: each is settled `cancelled` with no `durationMs` and never runs. Runs that have started are left alone (running, or held on a person or a timer); cancel one of those with `POST /accounts/{accountId}/runs/{runId}/cancel`. A run a worker takes at the same moment is either cancelled here or runs, never both. It is the Runs page's \"Cancel N waiting\", so it counts the customer's waiting editor tests too. Disabling a workflow leaves its waiting runs in the line; call this as well to drop them. Leaves the account's trail one `embed.run.cancel` row (target the workflow, metadata the `count`), none when nothing was waiting.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "workflowId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid"
      },
      "description": "The workflow's id, as the install or the list answered it."
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 255
      },
      "description": "Retry safely: the same key (per embed API key) with the same request answers the first response again, with `Idempotency-Replayed: true`, for 24 hours; only a success is kept, so a retry after a failure is a new try. The same key with a different request is 422 `VALIDATION_ERROR`, one over 255 characters is 400 `VALIDATION_ERROR`, and 409 `CONFLICT` says a request with this key is still running: retry shortly."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "How many waiting runs were cancelled (0 when none were waiting).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "cancelled"
           ],
           "properties": {
            "cancelled": {
             "type": "integer",
             "minimum": 0,
             "description": "The runs this call took out of the line and settled `cancelled`."
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — the `accountId` in the path is not valid or not escaped correctly (`error.details.field`: `account.id`). `VALIDATION_ERROR` — also an `Idempotency-Key` over 255 characters.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — no such account, or no such workflow in it.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "409": {
      "description": "code `ACCOUNT_DELETED` — the account is deleted (restorable for 30 days); restore it first. `CONFLICT` — a request with this `Idempotency-Key` is still running; retry shortly.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "413": {
      "description": "code `PAYLOAD_TOO_LARGE` — the body is over 5 MB. Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "415": {
      "description": "code `INVALID_CONTENT_TYPE` — a `Content-Type` other than `application/json` (leaving it out is fine). Nothing was done.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "422": {
      "description": "code `VALIDATION_ERROR` — this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/accounts/{accountId}/usage": {
   "get": {
    "operationId": "embedGetAccountUsage",
    "summary": "An account's usage over a range of days",
    "description": "Its production runs by the UTC day they started, sub-flow calls counted as part of the run that made them; its editor test runs separately; and the assistant tokens it used that the installation pays for. Counts never shrink when runs are deleted. They are rolled up every few minutes: `asOf` says how fresh they are.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "accountId",
      "in": "path",
      "required": true,
      "description": "The partner's account id (1–255 bytes of UTF-8, no control characters, case-sensitive). URL-escape it: an id may contain `/`.",
      "schema": {
       "type": "string",
       "minLength": 1,
       "maxLength": 255
      }
     },
     {
      "name": "from",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string",
       "format": "date"
      },
      "description": "The first UTC day counted (`YYYY-MM-DD`)."
     },
     {
      "name": "to",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string",
       "format": "date"
      },
      "description": "The last UTC day counted, inclusive: at most 366 days counting `from` and `to` themselves (365 after `from`), and no later than today."
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The account's usage.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/EmbedUsage"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "404": {
      "description": "code `NOT_FOUND` — the partner has no account with this id.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's or the account's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the tighter of the two windows.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/embed/v1/usage": {
   "get": {
    "operationId": "embedListUsage",
    "summary": "Every account's usage over a range of days",
    "description": "One entry per account, in every status — a deleted account inside its 30 days included — counted as `GET /accounts/{accountId}/usage` counts.",
    "tags": [
     "Embed: product"
    ],
    "x-embed-auth": "key",
    "parameters": [
     {
      "name": "from",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string",
       "format": "date"
      },
      "description": "The first UTC day counted (`YYYY-MM-DD`)."
     },
     {
      "name": "to",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string",
       "format": "date"
      },
      "description": "The last UTC day counted, inclusive: at most 366 days counting `from` and `to` themselves (365 after `from`), and no later than today."
     },
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "The `pagination.nextCursor` of the previous page. Omit for the first page."
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 100,
       "default": 50
      }
     }
    ],
    "security": [
     {
      "embedKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "A page of usage, one per account.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data",
          "pagination"
         ],
         "properties": {
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/EmbedUsage"
           }
          },
          "pagination": {
           "$ref": "#/components/schemas/EmbedCursorPagination"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "code `VALIDATION_ERROR` — `error.details.field` names the field; the message says what would be legal.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "401": {
      "description": "code `UNAUTHORIZED` — no embed API key (`Authorization: Bearer gre_…`), or one that was revoked, has expired after a rotation, or was mistyped.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "403": {
      "description": "code `EMBED_NOT_LICENSED` — this installation's license does not include embedding; `IP_NOT_ALLOWED` — the key's address allow-list does not include the caller.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "429": {
      "description": "code `RATE_LIMITED` — the key's requests this minute are used up. `Retry-After` says when the window resets; `X-RateLimit-Limit`/`X-RateLimit-Remaining` describe the key's window (`X-RateLimit-Scope: key`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     },
     "500": {
      "description": "code `INTERNAL_ERROR` — something this call needed could not be read or written on the server. Try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/EmbedError"
        }
       }
      }
     }
    }
   }
  },
  "/api/runs/counts": {
   "get": {
    "operationId": "countRuns",
    "summary": "Count runs by state, today and per day",
    "x-permission": "run:read",
    "description": "Every figure the Runs page shows, under the list's filters (not `state`, `status`, `waiting`, `taken`, `sort` or paging): runs in the range by state, the open runs at any age, today in `tz`, the newest run, and with `days=true` each local day's own counts. `queued`, `running` and `held` ignore the range (`window`, `from`, `to`): what is open now is listed whatever its age. Every other state, and the whole list with no `state`, follows it.",
    "parameters": [
     {
      "name": "triggerType",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "workflowId",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     },
     {
      "name": "tag",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "environment",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs` (default production).",
      "schema": {
       "type": "string",
       "enum": [
        "production",
        "test",
        "all"
       ]
      }
     },
     {
      "name": "window",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string",
       "enum": [
        "24h",
        "7d",
        "30d",
        "90d",
        "all"
       ]
      }
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string",
       "format": "date-time"
      }
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string",
       "format": "date-time"
      }
     },
     {
      "name": "tz",
      "in": "query",
      "required": false,
      "description": "IANA time zone for today and the days (UTC when absent or unknown).",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "q",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "reason",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "group",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "days",
      "in": "query",
      "required": false,
      "description": "`true` adds `days`.",
      "schema": {
       "type": "boolean"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The counts.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/RunCounts"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "A filter value this read does not know, or a malformed one, as on `GET /api/runs` (`triggerType`, `environment`, `window`, `from`/`to`, `workflowId`, `tag`, `q`, `reason`, `group`). Error code `VALIDATION_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The runs could not be read. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/runs/failures": {
   "get": {
    "operationId": "listRunFailureGroups",
    "summary": "Group failed runs by workflow, step and reason",
    "x-permission": "run:read",
    "description": "The failed runs in the range grouped by workflow, failed step and reason class (ids, quoted values and numbers taken out; an HTTP status kept). Still failing first, then newest; at most 100 groups. `q`, `reason` and `group` are checked as on `GET /api/runs`, then ignored.",
    "parameters": [
     {
      "name": "triggerType",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "workflowId",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     },
     {
      "name": "tag",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "environment",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs` (default production).",
      "schema": {
       "type": "string",
       "enum": [
        "production",
        "test",
        "all"
       ]
      }
     },
     {
      "name": "window",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string",
       "enum": [
        "24h",
        "7d",
        "30d",
        "90d",
        "all"
       ]
      }
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string",
       "format": "date-time"
      }
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string",
       "format": "date-time"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The groups.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "failed",
            "groupCount",
            "groups"
           ],
           "properties": {
            "failed": {
             "type": "integer"
            },
            "groupCount": {
             "type": "integer"
            },
            "groups": {
             "type": "array",
             "items": {
              "$ref": "#/components/schemas/RunFailureGroup"
             }
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "A filter value this read does not know, or a malformed one, as on `GET /api/runs` (`triggerType`, `environment`, `window`, `from`/`to`, `workflowId`, `tag`, `q`, `reason`, `group`). Error code `VALIDATION_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The runs could not be read. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/runs/range": {
   "get": {
    "operationId": "summarizeRunRange",
    "summary": "Summarize the runs in a range",
    "x-permission": "run:read",
    "description": "The range's runs and failures by cause (the 5 biggest groups), by local day, by the run's trigger type and the 10 busiest workflows, with how many failed runs a retry made good and how many finished runs took at least `slowerFactor` times their workflow's usual time.",
    "parameters": [
     {
      "name": "triggerType",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "workflowId",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     },
     {
      "name": "tag",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "environment",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs` (default production).",
      "schema": {
       "type": "string",
       "enum": [
        "production",
        "test",
        "all"
       ]
      }
     },
     {
      "name": "window",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string",
       "enum": [
        "24h",
        "7d",
        "30d",
        "90d",
        "all"
       ]
      }
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string",
       "format": "date-time"
      }
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string",
       "format": "date-time"
      }
     },
     {
      "name": "tz",
      "in": "query",
      "required": false,
      "description": "IANA time zone for today and the days (UTC when absent or unknown).",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "q",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "reason",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "group",
      "in": "query",
      "required": false,
      "description": "As on `GET /api/runs`.",
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "This range.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "properties": {
            "runs": {
             "type": "integer"
            },
            "failed": {
             "type": "integer"
            },
            "failedWorkedOnRetry": {
             "type": "integer"
            },
            "slowerThanUsual": {
             "type": "integer"
            },
            "slowerFactor": {
             "type": "integer"
            },
            "byCause": {
             "type": "array",
             "description": "The biggest failure groups (at most 5), biggest first.",
             "items": {
              "type": "object",
              "required": [
               "key",
               "workflowId",
               "workflowName",
               "reason",
               "count"
              ],
              "properties": {
               "key": {
                "type": "string",
                "description": "The failure group's `key` (`GET /api/runs?group={key}`)."
               },
               "workflowId": {
                "type": "string",
                "format": "uuid"
               },
               "workflowName": {
                "type": "string"
               },
               "step": {
                "type": "string",
                "description": "The failed step's display name; absent when the messages name no step."
               },
               "reason": {
                "type": "string"
               },
               "count": {
                "type": "integer"
               }
              }
             }
            },
            "byDay": {
             "type": "array",
             "description": "Each local day in `tz` with runs in the range.",
             "items": {
              "type": "object",
              "required": [
               "day",
               "from",
               "runs",
               "failed"
              ],
              "properties": {
               "day": {
                "type": "string",
                "format": "date"
               },
               "from": {
                "type": "string",
                "format": "date-time",
                "description": "The day's own start."
               },
               "runs": {
                "type": "integer"
               },
               "failed": {
                "type": "integer"
               }
              }
             }
            },
            "byTrigger": {
             "type": "array",
             "description": "Runs and failures by the run's own trigger type (as `GET /api/runs?triggerType`).",
             "items": {
              "type": "object",
              "required": [
               "triggerType",
               "runs",
               "failed"
              ],
              "properties": {
               "triggerType": {
                "type": "string"
               },
               "runs": {
                "type": "integer"
               },
               "failed": {
                "type": "integer"
               }
              }
             }
            },
            "byWorkflow": {
             "type": "array",
             "description": "The 10 busiest workflows.",
             "items": {
              "type": "object",
              "required": [
               "workflowId",
               "workflowName",
               "runs",
               "failed"
              ],
              "properties": {
               "workflowId": {
                "type": "string",
                "format": "uuid"
               },
               "workflowName": {
                "type": "string"
               },
               "runs": {
                "type": "integer"
               },
               "failed": {
                "type": "integer"
               }
              }
             }
            }
           },
           "required": [
            "runs",
            "failed",
            "failedWorkedOnRetry",
            "slowerThanUsual",
            "slowerFactor",
            "byCause",
            "byDay",
            "byTrigger",
            "byWorkflow"
           ]
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "A filter value this read does not know, or a malformed one, as on `GET /api/runs` (`triggerType`, `environment`, `window`, `from`/`to`, `workflowId`, `tag`, `q`, `reason`, `group`). Error code `VALIDATION_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The runs could not be read. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/runs/usual": {
   "get": {
    "operationId": "getUsualRunTimes",
    "summary": "Each workflow's usual run time",
    "x-permission": "run:read",
    "description": "The median `durationMs` of each workflow's newest 200 worked production runs of the last 30 days that never held on a person. A workflow with fewer than 5 such runs is absent. Keyed by workflow id.",
    "parameters": [
     {
      "name": "workflowId",
      "in": "query",
      "required": false,
      "description": "Repeatable, at most 200. Absent: every workflow of the workspace (the first 500 by id).",
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The usual times.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "additionalProperties": {
            "type": "object",
            "properties": {
             "p50Ms": {
              "type": "integer"
             },
             "runs": {
              "type": "integer"
             }
            }
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "More than 200 `workflowId`, or one that is not a UUID. Error code `VALIDATION_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The usual times could not be read. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/runs/{id}/rerun": {
   "post": {
    "operationId": "rerunRun",
    "summary": "Run a run again with the data it started with",
    "x-permission": "workflow:run",
    "description": "Starts a new run of the same workflow with this run's own trigger data, recorded as a retry of it (`retryOfRunId`). A production run runs again in production on the version published now; a test run runs again as a test of the version that ran (`executedVersionId`), or of the current draft when the draft ran. The run gate, the runs-at-once limits and the line's caps apply as on `POST /api/workflows/{id}/run`. Send an `Idempotency-Key` to make a repeat safe.",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid"
      },
      "description": "The run to run again."
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "description": "Opt-in replay protection, ≤255 characters. A retry carrying the same key and the same method, path, query and body replays the original answer instead of acting twice; the same key with a different request is refused with 422. Records live 24h.",
      "schema": {
       "type": "string",
       "maxLength": 255
      }
     }
    ],
    "responses": {
     "202": {
      "description": "Started.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/RunRerunStarted"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR`: the path id is not a run id (a UUID).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "`FORBIDDEN`: the token does not hold `workflow:run`. `FEATURE_OFF` (an embedded person): Run again is switched off, or a test run's retry while test runs are. `ACCOUNT_SUSPENDED`: the workspace's account is suspended, so nothing new starts in it until it is resumed. No run is started. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "Not a run of this workspace, or its workflow was deleted (`NOT_FOUND`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "`RUN_NOT_FINISHED` (queued, running or held: cancel it first, or wait), `WORKFLOW_NOT_LIVE` (a production run whose workflow is paused or a draft), `ACCOUNT_DELETED` (the workspace's account was deleted), or `CONFLICT` (a request with this `Idempotency-Key` is still in flight). No run is started.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "`PIECE_NOT_ALLOWED`: a step uses an app this workspace may not use. `VALIDATION_ERROR`: the definition that would run can't be read, or names an app or action the workers can't run. `VALIDATION_ERROR`: this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "`CONCURRENCY_LIMIT` — the workflow is at its `settings.concurrencyLimit` and chose `settings.whenFull: refuse`. `RUNS_WAITING_LIMIT` — the organization or this workflow has its cap of runs waiting in line. `RATE_LIMITED` — the rate limit, with `Retry-After`. No run is started.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The read or write failed. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PAUSED`: the installation is paused, so nothing can run. `PIECES_UNAVAILABLE`: which apps this workspace may use could not be read; nothing was started. `STATE_UNAVAILABLE`: the account's state could not be read, so nothing was started; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/runs/rerun": {
   "post": {
    "operationId": "rerunRunsBulk",
    "summary": "Run up to 200 runs again",
    "x-permission": "workflow:run",
    "description": "Each id as `POST /api/runs/{id}/rerun`, in the order given. Runs whose retry already worked or is still going are left out (`ALREADY_RETRIED`); once the organization's cap of waiting runs refuses one, the rest are not started, and once a workflow's own limit (`CONCURRENCY_LIMIT`, or its own cap of waiting runs) refuses one, that workflow's later ids are not started. Another workspace's ids, and ids that are not UUIDs, are `NOT_FOUND` and untouched.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/RunBulkRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Every id's answer.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/RunBulkResult"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "`VALIDATION_ERROR`: a body that is not `{\"ids\": [...]}`, no ids, or more than 200 (\"Send 1 to 200 run ids at a time.\").",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "`FORBIDDEN`: the token does not hold `workflow:run`. `FEATURE_OFF` (an embedded person): Run again is switched off. `ACCOUNT_SUSPENDED`: the workspace's account is suspended, so nothing new starts in it until it is resumed. Nothing is started. A token made inside a partner's product can also be refused as the shared `Forbidden` response says (`TOKEN_SURFACE_OFF`, `FEATURE_OFF`, `EMBED_NOT_ALLOWED`, `EMBED_NOT_AVAILABLE`, `ACCOUNT_SUSPENDED`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "`ACCOUNT_DELETED`: the workspace's account was deleted. `CONFLICT`: a request with this `Idempotency-Key` is still in flight. Nothing is started. (A refusal of one run, such as `RUN_NOT_FINISHED`, is that id's answer in the 200's `results`.)",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "`VALIDATION_ERROR`: this `Idempotency-Key` was already used with a different request.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again. (The run line's caps refuse single ids inside the 200's `results`.)",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The read or write failed. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "`PAUSED`: the installation is paused, so nothing can run. `STATE_UNAVAILABLE`: the account's state could not be read, so nothing was started; try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ],
    "parameters": [
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "description": "Opt-in replay protection, ≤255 characters. A retry carrying the same key and the same method, path, query and body replays the original answer instead of acting twice; the same key with a different request is refused with 422. Records live 24h.",
      "schema": {
       "type": "string",
       "maxLength": 255
      }
     }
    ]
   }
  },
  "/api/runs/cancel": {
   "post": {
    "operationId": "cancelRunsBulk",
    "summary": "Cancel up to 200 runs",
    "x-permission": "run:manage",
    "description": "Each id as `POST /api/runs/{id}/cancel`: a queued run leaves the line and never runs, a held run is settled, a running one is asked to stop; a finished run is left as it is (`ok: true, cancelled: false`). Another workspace's ids, and ids that are not UUIDs, are `NOT_FOUND` and untouched.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/RunBulkRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Every id's answer.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "$ref": "#/components/schemas/RunBulkResult"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "A body that is not `{\"ids\": [...]}`, no ids, or more than 200 (`VALIDATION_ERROR`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "415": {
      "description": "`INVALID_CONTENT_TYPE` — a Content-Type was sent and it was not `application/json`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded (code `RATE_LIMITED`). `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "The read or write failed. Error code `INTERNAL_ERROR`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "tags": [
     "Runs"
    ]
   }
  },
  "/api/webhooks/resume/{token}": {
   "parameters": [
    {
     "name": "token",
     "in": "path",
     "required": true,
     "description": "The token in the link: the step's `resumeToken`, and the end of its `resumeUrl`.",
     "schema": {
      "type": "string"
     }
    },
    {
     "name": "decision",
     "in": "query",
     "required": false,
     "description": "On an approval: `approve`/`approved` or `reject`/`rejected`/`decline`/`declined`, in any case, decide it. Every query parameter also reaches the step in `resumeData`.",
     "schema": {
      "type": "string"
     }
    }
   ],
   "post": {
    "operationId": "answerResumeLink",
    "tags": [
     "Runs"
    ],
    "summary": "Answer a waiting run's link",
    "security": [],
    "description": "Carries on a run held on a Wait for Approval / Webhook step or an Approval step. **It takes no API token**: the token in the address is the credential, handed out by the step as `resumeUrl` (or `approveUrl` and `rejectUrl`), and anyone holding it can answer. A link works once.\n\nThe query and the body become the step's `resumeData`: a JSON object's keys beside the query's (the body wins a clash), anything else under `body`. Any content type, up to 5 MB.\n\nA token-holding caller deciding from inside the product uses `POST /api/runs/{id}/decision` instead.",
    "requestBody": {
     "required": false,
     "description": "Any content type.",
     "content": {
      "*/*": {
       "schema": {}
      }
     }
    },
    "responses": {
     "200": {
      "description": "Recorded: the run carries on. A caller asking for `text/html` gets a page instead of this body.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "runId",
            "status",
            "waitStep"
           ],
           "properties": {
            "runId": {
             "type": "string",
             "format": "uuid"
            },
            "status": {
             "type": "string",
             "enum": [
              "resuming"
             ],
             "description": "Always `resuming`: read the run for what happens next."
            },
            "waitStep": {
             "type": "string",
             "description": "The `name` of the step that was waiting."
            }
           }
          }
         }
        },
        "example": {
         "data": {
          "runId": "6054da78-5dd5-42fe-bd5e-99edfc1185e8",
          "status": "resuming",
          "waitStep": "wait_for_answer"
         }
        }
       },
       "text/html": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR: a NUL in the address, or a body that could not be read. Nothing was decided and the link still works.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "The link answers nothing. `NOT_FOUND`: never issued, cut short, or its account was deleted. `ALREADY_ANSWERED`: it worked once already; the message says how and when. `TIMED_OUT`: the step's maximum wait ended and the run went on without an answer. For `text/html`, a page saying the same, with this status.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "405": {
      "description": "METHOD_NOT_ALLOWED: anything but `GET`, `HEAD`, `POST` and `OPTIONS`. `Allow` lists them.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "ACCOUNT_SUSPENDED: the run's account is suspended. The link is left as it was and works once the account is active.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "413": {
      "description": "PAYLOAD_TOO_LARGE: the body is over 5 MB. Nothing was decided and the link still works.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "RATE_LIMITED: this address sent more than `WEBHOOK_RATE_LIMIT` requests this minute, counted with the webhook group. `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "ENQUEUE_FAILED: the answer could not be queued. The link still works: send it again. (For `text/html`, a page.)",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "Nothing was decided. `SERVICE_UNAVAILABLE` (`Retry-After: 5`): the link could not be looked up just now. `PAUSED` (`Retry-After: 3600`): a self-hosted installation without a license.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   },
   "get": {
    "operationId": "openResumeLink",
    "tags": [
     "Runs"
    ],
    "summary": "Open a waiting run's link",
    "security": [],
    "description": "A caller asking for `text/html` (a person, a mail scanner, a link preview) gets a page with a button and nothing is decided; the button sends the `POST`. Any other caller answers the link, exactly as a `POST` with no body does. `HEAD` answers as the page does and never decides; `OPTIONS` is an empty 204 with `Allow`.",
    "responses": {
     "200": {
      "description": "Recorded: the run carries on. A caller asking for `text/html` gets a page instead of this body.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "runId",
            "status",
            "waitStep"
           ],
           "properties": {
            "runId": {
             "type": "string",
             "format": "uuid"
            },
            "status": {
             "type": "string",
             "enum": [
              "resuming"
             ],
             "description": "Always `resuming`: read the run for what happens next."
            },
            "waitStep": {
             "type": "string",
             "description": "The `name` of the step that was waiting."
            }
           }
          }
         }
        },
        "example": {
         "data": {
          "runId": "6054da78-5dd5-42fe-bd5e-99edfc1185e8",
          "status": "resuming",
          "waitStep": "wait_for_answer"
         }
        }
       },
       "text/html": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "400": {
      "description": "VALIDATION_ERROR: a NUL in the address, or a body that could not be read. Nothing was decided and the link still works.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "The link answers nothing. `NOT_FOUND`: never issued, cut short, or its account was deleted. `ALREADY_ANSWERED`: it worked once already; the message says how and when. `TIMED_OUT`: the step's maximum wait ended and the run went on without an answer. For `text/html`, a page saying the same, with this status.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "405": {
      "description": "METHOD_NOT_ALLOWED: anything but `GET`, `HEAD`, `POST` and `OPTIONS`. `Allow` lists them.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "409": {
      "description": "ACCOUNT_SUSPENDED: the run's account is suspended. The link is left as it was and works once the account is active.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "413": {
      "description": "PAYLOAD_TOO_LARGE: the body is over 5 MB. Nothing was decided and the link still works.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "RATE_LIMITED: this address sent more than `WEBHOOK_RATE_LIMIT` requests this minute, counted with the webhook group. `Retry-After` says when to try again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "500": {
      "description": "ENQUEUE_FAILED: the answer could not be queued. The link still works: send it again. (For `text/html`, a page.)",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "Nothing was decided. `SERVICE_UNAVAILABLE` (`Retry-After: 5`): the link could not be looked up just now. `PAUSED` (`Retry-After: 3600`): a self-hosted installation without a license.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/health": {
   "get": {
    "operationId": "getHealth",
    "tags": [
     "Service"
    ],
    "summary": "Is the process up",
    "security": [],
    "description": "200 while the API process runs, whatever its database and Redis are doing: use `GET /api/ready` to route traffic. Not rate limited. On GoRunner Cloud and an unstamped build the answer names the `version`. A self-hosted release build names `version` and `edition` only to a request from the server itself with no forwarding header, or to anyone with `HEALTH_DETAILS=public`.",
    "responses": {
     "200": {
      "description": "Up.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "data"
         ],
         "properties": {
          "data": {
           "type": "object",
           "required": [
            "status"
           ],
           "properties": {
            "status": {
             "type": "string",
             "enum": [
              "ok"
             ]
            },
            "version": {
             "type": "string"
            },
            "edition": {
             "type": "string",
             "enum": [
              "selfhosted"
             ]
            }
           }
          }
         }
        },
        "example": {
         "data": {
          "status": "ok",
          "version": "v2026.10.54"
         }
        }
       }
      }
     },
     "405": {
      "description": "METHOD_NOT_ALLOWED: only `GET`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/ready": {
   "get": {
    "operationId": "getReady",
    "tags": [
     "Service"
    ],
    "summary": "Can it do its work",
    "security": [],
    "description": "Whether this API process reaches its database and Redis, each within 2 seconds; the result is reused for a second. **No `data` envelope.** `version` and `checks` follow the same rule as `GET /api/health`. A check names its failure in a fixed phrase, never an address or a user.",
    "responses": {
     "200": {
      "description": "Ready.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ReadyAnswer"
        },
        "example": {
         "status": "ok",
         "version": "v2026.10.54",
         "checks": {
          "database": {
           "ok": true,
           "ms": 3
          },
          "redis": {
           "ok": true,
           "ms": 0
          }
         }
        }
       }
      }
     },
     "405": {
      "description": "METHOD_NOT_ALLOWED: only `GET`.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "Not ready.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ReadyAnswer"
        },
        "example": {
         "status": "unavailable",
         "version": "v2026.10.54",
         "checks": {
          "database": {
           "ok": true,
           "ms": 8
          },
          "redis": {
           "ok": false,
           "ms": 62,
           "error": "connection refused"
          }
         }
        }
       }
      }
     }
    }
   }
  }
 },
 "components": {
  "securitySchemes": {
   "bearerAuth": {
    "type": "http",
    "scheme": "bearer",
    "description": "An API token: `Authorization: Bearer grt_…`. Make one in the app under Settings › Workspace › API tokens; the raw value is shown once. Scopes confine the token — see `x-permission` on each operation."
   },
   "embedKey": {
    "type": "http",
    "scheme": "bearer",
    "description": "An embed API key for the partner's server: `Authorization: Bearer gre_…`. Made in Platform › Partner keys; the raw value is shown once. Optional address allow-list. Never use it from a browser."
   },
   "embedSession": {
    "type": "http",
    "scheme": "bearer",
    "description": "An embed session for the embedded frame: `Authorization: Bearer gri_…`, from `POST /api/embed/v1/sessions`."
   }
  },
  "responses": {
   "Unauthorized": {
    "description": "No API token, or one that does not work. `UNAUTHORIZED`: the `Authorization: Bearer grt_…` header is missing, malformed or names a token that is unknown, revoked or expired. A token made inside a partner's product (in its embedded view, or by its server) that no longer works answers `TOKEN_ENDED` instead, with `details.reason`: `revoked`, `expired`, `access_removed` (its maker left the account), `role_changed` (its maker is no longer an editor), `account_deleted` or `workspace_deleted`. Either way, retrying does not help: make a new token. A token the server could not check just now is a 503 `SERVICE_UNAVAILABLE` with `Retry-After`, not a 401.",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "Forbidden": {
    "description": "The token may not do this. `FORBIDDEN`: it does not hold the scope this operation's `x-permission` names (`details.permission` names it). A token made inside a partner's product can also answer: `TOKEN_SURFACE_OFF` (`details.surface`: `api`), it was not made to work with the API; `FEATURE_OFF` (`details.switch`), its account has the `tokens` switch, or a switch this operation needs, turned off; `EMBED_NOT_ALLOWED`, the operation is not one an embedded account may use; `EMBED_NOT_AVAILABLE` (`details.reason`: `not_licensed` or `sign_in_off`), the installation's embed is not available right now; `ACCOUNT_SUSPENDED`, a change while its account is suspended (it can still read).",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   }
  },
  "schemas": {
   "TemplateRow": {
    "type": "object",
    "description": "A template as the Templates page lists it: everything it draws. `definition` and `createdBy` are the fields a row had before the Templates redesign: sent by default, absent with `view=summary`.",
    "required": [
     "id",
     "scope",
     "name",
     "description",
     "category",
     "triggerType",
     "createdAt",
     "updatedAt",
     "readable",
     "author",
     "source",
     "savedHere",
     "savedIn",
     "sourceWorkflow",
     "trigger",
     "marks",
     "steps",
     "whenLine",
     "slots",
     "toFill",
     "available",
     "unavailable",
     "uses",
     "canManage"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "scope": {
      "type": "string",
      "enum": [
       "org",
       "workspace",
       "installation"
      ],
      "description": "Who sees it: `org` — an organization's, every workspace of it; `workspace` — a customer account's, the workspace it was saved in only; `installation` — shared by the installation with its customer accounts (listed only in a customer account, and only while the license has embedding), read-only here."
     },
     "key": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",
      "description": "An installation template's stable handle. Absent on every other template."
     },
     "name": {
      "type": "string"
     },
     "description": {
      "type": "string",
      "description": "What it does, as saved. May be empty: `whenLine` then describes it."
     },
     "category": {
      "type": "string",
      "description": "Free-text grouping label; empty when none."
     },
     "triggerType": {
      "type": "string"
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "updatedAt": {
      "type": "string",
      "format": "date-time"
     },
     "publishedAt": {
      "type": "string",
      "format": "date-time",
      "description": "When an installation template was first offered. Absent on every other template."
     },
     "readable": {
      "type": "boolean",
      "description": "false: its definition cannot be read. Such a row is listed only to someone who may delete it (`canManage`), with everything derived from the definition empty."
     },
     "author": {
      "type": [
       "object",
       "null"
      ],
      "description": "Who saved it; null for an installation template.",
      "required": [
       "id",
       "name",
       "left"
      ],
      "properties": {
       "id": {
        "type": [
         "string",
         "null"
        ],
        "format": "uuid"
       },
       "name": {
        "type": [
         "string",
         "null"
        ],
        "description": "Their name, else their email; null once the account is deleted."
       },
       "left": {
        "type": "boolean",
        "description": "They are no longer a member of this organization."
       }
      }
     },
     "source": {
      "type": [
       "object",
       "null"
      ],
      "description": "An installation template's origin; null otherwise.",
      "required": [
       "organization"
      ],
      "properties": {
       "organization": {
        "type": "string",
        "description": "The organization it was shared from; empty once that organization is gone."
       }
      }
     },
     "savedHere": {
      "type": "boolean",
      "description": "It was saved in this workspace. Otherwise its bound values arrive blank (see `TemplateFillField`)."
     },
     "savedIn": {
      "type": [
       "string",
       "null"
      ],
      "description": "The workspace it was saved in (an installation template: the organization it was shared from); null when not recorded."
     },
     "sourceWorkflow": {
      "type": [
       "object",
       "null"
      ],
      "description": "The workflow it was saved (or last updated) from, while that workflow exists and is in this workspace.",
      "required": [
       "id",
       "name"
      ],
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string"
       }
      }
     },
     "trigger": {
      "type": [
       "object",
       "null"
      ],
      "required": [
       "name",
       "displayName",
       "pieceId",
       "actionId"
      ],
      "properties": {
       "name": {
        "type": "string"
       },
       "displayName": {
        "type": "string"
       },
       "pieceId": {
        "type": "string"
       },
       "actionId": {
        "type": "string"
       }
      }
     },
     "marks": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "App ids in run order: the trigger's, then one per step (branch steps `branch`, loops `loop`, a Code step `code`), never de-duplicated."
     },
     "steps": {
      "type": "array",
      "description": "Every step, in the order of `marks` after the trigger.",
      "items": {
       "type": "object",
       "required": [
        "name",
        "displayName",
        "pieceId",
        "actionId",
        "type",
        "depth"
       ],
       "properties": {
        "name": {
         "type": "string"
        },
        "displayName": {
         "type": "string"
        },
        "pieceId": {
         "type": "string"
        },
        "actionId": {
         "type": "string"
        },
        "type": {
         "type": "string",
         "enum": [
          "PIECE",
          "CODE",
          "BRANCH",
          "LOOP"
         ]
        },
        "depth": {
         "type": "integer",
         "description": "0 at the top level; one more inside a branch arm, a loop body or an error path."
        }
       }
      }
     },
     "whenLine": {
      "type": "string",
      "description": "A generated line for a template without a description: how it starts, then its first three steps' names."
     },
     "slots": {
      "type": "array",
      "description": "The connections a workflow made from it needs, one per service account.",
      "items": {
       "type": "object",
       "required": [
        "key",
        "pieceId",
        "pieceName",
        "steps",
        "stepLabels",
        "connectionId"
       ],
       "properties": {
        "key": {
         "type": "string"
        },
        "pieceId": {
         "type": "string",
         "description": "The connection's app (what `GET /api/connections` rows carry)."
        },
        "pieceName": {
         "type": "string"
        },
        "steps": {
         "type": "array",
         "items": {
          "type": "string"
         }
        },
        "stepLabels": {
         "type": "array",
         "items": {
          "type": "string"
         }
        },
        "connectionId": {
         "type": [
          "string",
          "null"
         ],
         "description": "The connection the template's own steps name; null for an installation template or none."
        }
       }
      }
     },
     "toFill": {
      "type": "integer",
      "description": "How many values using it here will ask for: required fields left blank and bound values blanked here."
     },
     "available": {
      "type": "boolean"
     },
     "unavailable": {
      "type": [
       "object",
       "null"
      ],
      "description": "Why it cannot be used on this installation.",
      "required": [
       "code",
       "message"
      ],
      "properties": {
       "code": {
        "type": "string",
        "enum": [
         "PIECE_UNAVAILABLE",
         "STEP_INCOMPLETE",
         "ACTION_UNAVAILABLE",
         "UNREADABLE"
        ]
       },
       "message": {
        "type": "string"
       },
       "step": {
        "type": "string",
        "description": "The step it is about, by name."
       }
      }
     },
     "uses": {
      "type": [
       "object",
       "null"
      ],
      "description": "Workflows of this organization made from it, and how many are live. Sent only to its author, an organization admin or a platform admin.",
      "required": [
       "workflows",
       "live"
      ],
      "properties": {
       "workflows": {
        "type": "integer"
       },
       "live": {
        "type": "integer"
       }
      }
     },
     "canManage": {
      "type": "boolean",
      "description": "The caller may rename, update the steps of and delete it: its creator, an organization admin, or for a `workspace` template that workspace's admin."
     },
     "createdBy": {
      "type": "string",
      "format": "uuid",
      "description": "Who saved it (`author` names them). Absent on an installation template, when the account is gone, and with `view=summary`."
     },
     "definition": {
      "$ref": "#/components/schemas/TemplateFlowDefinition"
     }
    }
   },
   "TemplateBlankedValue": {
    "type": "object",
    "required": [
     "step",
     "stepLabel",
     "field",
     "fieldLabel"
    ],
    "properties": {
     "step": {
      "type": "string",
      "description": "The step's name (the trigger's own name for the trigger)."
     },
     "stepLabel": {
      "type": "string",
      "description": "The step as the canvas names it."
     },
     "field": {
      "type": "string",
      "description": "The top-level input key."
     },
     "fieldLabel": {
      "type": "string",
      "description": "The app's title for the input."
     }
    }
   },
   "TemplateFillField": {
    "type": "object",
    "description": "A value using the template asks for. `why`: `bound` — a value that points into the account the template was saved with (a dropdown filled from the account, or an input the catalog marks `bound`), blank here because it was saved in another workspace, `hint` its old value; `check` — the same, kept because it was saved here; `required` — a required field the template leaves blank. An expression is never blanked.",
    "required": [
     "step",
     "stepLabel",
     "pieceId",
     "actionId",
     "field",
     "fieldLabel",
     "propertyType",
     "required",
     "why",
     "slot",
     "value",
     "hint"
    ],
    "properties": {
     "step": {
      "type": "string"
     },
     "stepLabel": {
      "type": "string"
     },
     "pieceId": {
      "type": "string"
     },
     "actionId": {
      "type": "string"
     },
     "field": {
      "type": "string"
     },
     "fieldLabel": {
      "type": "string"
     },
     "propertyType": {
      "type": "string"
     },
     "required": {
      "type": "boolean"
     },
     "why": {
      "type": "string",
      "enum": [
       "bound",
       "check",
       "required"
      ]
     },
     "slot": {
      "type": [
       "string",
       "null"
      ],
      "description": "The slot of the step's connection."
     },
     "value": {
      "description": "What the workflow would get, with the request's `values` applied."
     },
     "hint": {
      "description": "The old value of a blanked bound field; null otherwise."
     }
    }
   },
   "TemplateValues": {
    "type": "object",
    "description": "The person's fill-ins: step name → input key → value, written over the template's values. A blank value (`\"\"`, null, `[]`) leaves the field blank. A step or input the template does not have is refused 400 (`details.field: values`, `details.step`, `details.input`).",
    "additionalProperties": {
     "type": "object",
     "additionalProperties": {}
    },
    "example": {
     "write_kpis": {
      "sheet_documentId": "1xMySheet"
     }
    }
   },
   "TemplateSavePreview": {
    "type": "object",
    "required": [
     "scope",
     "blanked",
     "bound"
    ],
    "properties": {
     "scope": {
      "type": "string",
      "enum": [
       "org",
       "workspace"
      ],
      "description": "Who would get it."
     },
     "blanked": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TemplateBlankedValue"
      },
      "description": "Each value saving would leave out."
     },
     "bound": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TemplateBlankedValue"
      },
      "description": "Values that travel but arrive blank in any other workspace."
     }
    }
   },
   "TemplateStepsChange": {
    "type": "object",
    "required": [
     "steps",
     "apps",
     "marks",
     "blanked"
    ],
    "properties": {
     "steps": {
      "type": "object",
      "required": [
       "now",
       "after"
      ],
      "properties": {
       "now": {
        "type": "integer"
       },
       "after": {
        "type": "integer"
       }
      }
     },
     "apps": {
      "type": "object",
      "required": [
       "now",
       "after"
      ],
      "properties": {
       "now": {
        "type": "array",
        "items": {
         "type": "string"
        }
       },
       "after": {
        "type": "array",
        "items": {
         "type": "string"
        }
       }
      }
     },
     "marks": {
      "type": "object",
      "required": [
       "now",
       "after"
      ],
      "properties": {
       "now": {
        "type": "array",
        "items": {
         "type": "string"
        }
       },
       "after": {
        "type": "array",
        "items": {
         "type": "string"
        }
       }
      }
     },
     "blanked": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TemplateBlankedValue"
      }
     }
    }
   },
   "Error": {
    "type": "object",
    "required": [
     "error"
    ],
    "description": "The failure envelope. Branch on `error.code`.",
    "properties": {
     "error": {
      "type": "object",
      "required": [
       "code",
       "message"
      ],
      "properties": {
       "code": {
        "type": "string",
        "description": "A stable SCREAMING_SNAKE identifier.",
        "examples": [
         "VALIDATION_ERROR",
         "NOT_FOUND",
         "FORBIDDEN",
         "CONFLICT",
         "RATE_LIMITED"
        ]
       },
       "message": {
        "type": "string",
        "description": "Written for a human, in the installation's own words for workflows, runs, connections and templates (an installation that renames them answers with its names). May change; never branch on it."
       },
       "details": {
        "type": "object",
        "additionalProperties": true,
        "description": "Present on some errors: an object a program can act on. Its keys depend on the code, for example the field that is wrong (`field`), the permission that is missing (`permission`), the switch that is off (`switch`), the limit (`limit`, `max`) or why a session or token ended (`reason`)."
       }
      }
     }
    }
   },
   "RunStatus": {
    "type": "string",
    "description": "A run's lifecycle status. TERMINAL values — stop polling when you see one — are `completed`, `failed` and `cancelled`. NON-TERMINAL values are `waiting` (accepted and waiting in line for a free slot: its workflow's or its organization's runs-at-once limit, or a free worker — `waitingFor`, `waitingLimit` and `linePosition` say which and where; it starts in order, and cancelling it takes it out of the line), `pending` (a worker has it or is about to: `claimedAt` says when a worker took it), `running` (a resumed run executing after its wait) and `paused` (suspended on a durable wait: an approval/webhook resume, or a scheduled resume time). A paused run can remain paused indefinitely; it resumes only when its wait is satisfied, or ends if you cancel it. `waiting` is new in v2026.10.4: before it, a run over a limit was refused instead.",
    "enum": [
     "waiting",
     "pending",
     "running",
     "completed",
     "failed",
     "cancelled",
     "paused"
    ]
   },
   "StepRunStatus": {
    "type": "string",
    "description": "A single step's status inside a run. `skipped` means the step was not executed (a branch arm not taken, or a step after a failure). `paused` marks the wait step a suspended run is currently sitting on. A step executing when its run is cancelled ends `failed` (\"Cancelled while it ran.\"), and so does the step a cancelled run was held on. A step can be `failed` while the run is `completed` — that happens when the step's error handling routed to an error path or continued.",
    "enum": [
     "pending",
     "running",
     "completed",
     "failed",
     "skipped",
     "paused"
    ]
   },
   "StepRun": {
    "type": "object",
    "description": "One executed step of a run. Addressed by `nodeId`, which is the step's NAME in the workflow definition (`FlowAction.name`) — the same identifier expressions use as `{{steps.<nodeId>.field}}`. This is the object that carries step output.",
    "required": [
     "id",
     "runId",
     "nodeId",
     "pieceId",
     "actionId",
     "displayName",
     "status",
     "retryCount"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid",
      "description": "Id of this step-run row. A step that suspended the run gets a new id when the run resumes: address a step by `nodeId`."
     },
     "runId": {
      "type": "string",
      "format": "uuid",
      "description": "The run this step belongs to."
     },
     "nodeId": {
      "type": "string",
      "description": "The definition step NAME (stable identifier, e.g. `parse_csv`). Use this to find a step, not `displayName`."
     },
     "pieceId": {
      "type": "string",
      "description": "The app that ran, e.g. `http`, `code`, `csv`."
     },
     "actionId": {
      "type": "string",
      "description": "The action within that app, e.g. `send_request`, `convert`."
     },
     "displayName": {
      "type": "string",
      "description": "The human label shown on the canvas. Not an identifier — it can be duplicated and renamed."
     },
     "status": {
      "$ref": "#/components/schemas/StepRunStatus"
     },
     "inputData": {
      "description": "The resolved input the step ran with, after expressions were evaluated. Normally an object of the form `{\"config\": { ...the step's resolved config... }}`. Absent when the step never executed, and on a step that suspended the run (a top-level Approval, Delay or Wait for callback) once the run has resumed: its `outputData` is kept, its input is not.",
      "type": [
       "object",
       "array",
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "outputData": {
      "description": "THE STEP'S OUTPUT — whatever the app returned, arbitrary JSON. Absent when the step failed before producing output or never executed. Large payloads are offloaded to object storage during the run and rehydrated here transparently, so this is always inline JSON when present; when it cannot be read back, it is absent and `dataUnavailable` is true.",
      "type": [
       "object",
       "array",
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "errorMessage": {
      "type": "string",
      "description": "Why the step failed. Absent/empty when it did not."
     },
     "retryCount": {
      "type": "integer",
      "description": "Which attempt this record reflects (0 = first attempt)."
     },
     "startedAt": {
      "type": "string",
      "format": "date-time",
      "description": "When the step began. Absent if it never started."
     },
     "finishedAt": {
      "type": "string",
      "format": "date-time",
      "description": "When the step settled. Absent while running or if it never finished."
     },
     "durationMs": {
      "type": "integer",
      "description": "How long this record's attempt took, in milliseconds, rounded (0 = under half a millisecond; the dashboard says \"<1 ms\"). For a retried step it is the last attempt; for a step that ran more than once (a loop body) its last pass. `totalDurationMs` has the step's time in all."
     },
     "attempts": {
      "type": "integer",
      "minimum": 2,
      "description": "How many attempts the step's last pass took, when it was retried (absent: one attempt; every field of this group is absent on runs recorded before it existed). This record is the last of them."
     },
     "firstAttemptStartedAt": {
      "type": "string",
      "format": "date-time",
      "description": "When the first of `attempts` began. Present with `attempts`."
     },
     "passes": {
      "type": "integer",
      "minimum": 2,
      "description": "How many times the step ran in the run, when more than once — a step inside a loop runs once per item. Absent: once. This record is the last pass."
     },
     "totalDurationMs": {
      "type": "integer",
      "format": "int64",
      "description": "The step's time in all, in milliseconds: each pass from its first attempt's start to its finish (the waits between retries included), summed over its passes. Present with `attempts` or `passes`."
     },
     "attemptErrors": {
      "type": "array",
      "description": "The earlier attempts of the last pass and why each failed, oldest first: at most the last 20, each error at most 1,000 characters. Present with `attempts`.",
      "items": {
       "type": "object",
       "required": [
        "attempt",
        "error",
        "at"
       ],
       "properties": {
        "attempt": {
         "type": "integer",
         "minimum": 1,
         "description": "Which attempt (1 = the first)."
        },
        "error": {
         "type": "string",
         "description": "What it failed with."
        },
        "at": {
         "type": "string",
         "format": "date-time",
         "description": "When it ended."
        }
       }
      }
     },
     "recovery": {
      "type": "string",
      "enum": [
       "replayed",
       "rerun",
       "unknown"
      ],
      "description": "In a restarted run (`restarts`), what the run did with this step: `replayed`, an earlier attempt finished it, its recorded result was used and it did not run again; `rerun`, it had started when the worker stopped and ran again (it only reads or computes); `unknown`, it changes something outside the run and was in flight when the worker stopped, so it was NOT sent again: it failed with \"Outcome unknown\" and its error handling decided what followed. Absent: an ordinary step."
     },
     "runAttempt": {
      "type": "integer",
      "minimum": 2,
      "description": "Which execution of a restarted run produced this record, when it was not the first (2 = the first restart). Absent: the first."
     },
     "dataUnavailable": {
      "type": "boolean",
      "description": "Present (true) when the step's payload was offloaded to object storage and could not be read back: the step did produce `inputData`/`outputData`, but it cannot be shown now (they are absent). Tells \"produced nothing\" apart from \"cannot be read\". Absent otherwise."
     },
     "deferred": {
      "type": "object",
      "description": "Only on a read with `large=defer`: the step's `inputData` and/or `outputData` this read left out because each is over 64 KB of JSON (the step a paused run waits on is never left out), each with its size. The value exists; read it with `GET /api/runs/{id}?step=<nodeId>`. Absent otherwise.",
      "properties": {
       "input": {
        "$ref": "#/components/schemas/DeferredValue"
       },
       "output": {
        "$ref": "#/components/schemas/DeferredValue"
       }
      }
     }
    }
   },
   "Run": {
    "type": "object",
    "description": "One execution of a workflow. The same schema serves the list and the detail read, but the two carry different subsets: the LIST adds `workflowName`, `workflowTriggerType`, `stepTotal`, `stepFailed` and the fields marked LIST only, and never includes `steps`; the DETAIL includes `steps`. Both carry the version fields and the retry and parent links. An IN-FLIGHT run read from the detail endpoint is served from a live cache and is completed from its run row (its start, environment, versions, retry and parent links, workspace and organization), so it reads as a settled run does, except that `tags` is `null` until the worker saves it.",
    "required": [
     "id",
     "workflowId",
     "status",
     "triggerType",
     "startedAt"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid",
      "description": "The run id."
     },
     "workflowId": {
      "type": "string",
      "format": "uuid",
      "description": "The workflow that ran."
     },
     "workspaceId": {
      "type": "string",
      "format": "uuid",
      "description": "Tenant workspace, denormalized onto the run."
     },
     "orgId": {
      "type": "string",
      "format": "uuid",
      "description": "Tenant organization."
     },
     "status": {
      "$ref": "#/components/schemas/RunStatus"
     },
     "triggerType": {
      "type": "string",
      "description": "How THIS run began — an open string, not the workflow's trigger vocabulary. Known values: `manual` (editor test), `webhook`, `cron`, `poll`, `event`, `error` (an error-handler workflow), `run_finished`, `api` (started through `POST /api/workflows/{id}/run`), `mcp` (the same door, by an AI app's tool), `subflow` (started by a parent flow's Sub-flow step)."
     },
     "triggerData": {
      "description": "The payload the trigger delivered — for a webhook run an object with `body`, `headers`, `query`, `method`. This is what `{{trigger...}}` expressions resolved against.",
      "type": [
       "object",
       "array",
       "string",
       "number",
       "boolean",
       "null"
      ]
     },
     "startedAt": {
      "type": "string",
      "format": "date-time",
      "description": "When the run was accepted. It may then wait in line for a free slot before a worker starts it (`firstStartedAt`)."
     },
     "firstStartedAt": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "When a worker first started the run. `durationMs` counts from here, so the time from `startedAt` to this is the time the run waited in line. Null until a worker starts the run, and for a run started on v2026.10.5 or earlier or one that ran inline (a Sub-flow's, an error handler's), whose `durationMs` counts from `startedAt`."
     },
     "finishedAt": {
      "type": "string",
      "format": "date-time",
      "description": "When the run settled: the moment its result was decided (not when its record was saved, which can be a moment later). Present as soon as the run is finished, also while its record is still being saved. Absent while non-terminal."
     },
     "durationMs": {
      "type": "integer",
      "description": "How long the run took, in milliseconds: from the first time a worker started the run (`firstStartedAt`, else `startedAt`) to its finish. Waits inside the run (a Delay, an Approval) count; the time it waited in the line for a free slot does not. A run cancelled while it waited has none. Rounded to the millisecond. Present as soon as the run is finished, also while its record is still being saved. Absent while non-terminal."
     },
     "errorMessage": {
      "type": "string",
      "description": "Why the run failed (or the cancellation reason). Absent/empty otherwise."
     },
     "tags": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "type": "string"
      },
      "description": "Tags attached during execution by the Tags app. Filterable via `GET /api/runs?tag=`. `[]` when it has none. `null` while the run is read from its live cache: in flight until the worker saves it, and for a moment after it is cancelled. A run still waiting in line, read from its record, says `[]`."
     },
     "environment": {
      "type": "string",
      "enum": [
       "production",
       "test"
      ],
      "description": "Where the run came from. `test` for anything the editor started; `production` for real trigger traffic and for `POST /api/workflows/{id}/run?environment=production`. Deliberately independent of `triggerType` — a listen-captured run is both a webhook run and a test run. `GET /api/runs` hides `test` runs unless asked."
     },
     "steps": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/StepRun"
      },
      "description": "The executed steps, oldest first. Returned by `GET /api/runs/{id}` only — never in the list. Step output lives here, keyed by each element's `nodeId`."
     },
     "versionId": {
      "type": "string",
      "format": "uuid",
      "description": "The workflow's latest published version at the moment the run started. Absent for a workflow that has never been published."
     },
     "versionNumber": {
      "type": "integer",
      "description": "Display number of `versionId`."
     },
     "executedVersionId": {
      "type": "string",
      "format": "uuid",
      "description": "The version whose definition ACTUALLY executed. ABSENT means the DRAFT ran (an editor test), so rendering `versionId`'s snapshot would show a graph that never ran. The list and the detail read carry it; `GET /api/workflows/{id}/runs` does not."
     },
     "executedVersionNumber": {
      "type": "integer",
      "description": "Display number of `executedVersionId`."
     },
     "startedFromStep": {
      "type": "string",
      "description": "For a partial (\"run from here\") run, the definition step NAME the run began at. Absent means the run started at its trigger, which is every ordinary run. Everything above that step was seeded, not executed — do not read its steps as having run. Detail read only: a row of `GET /api/runs` leaves it out."
     },
     "retryOfRunId": {
      "type": "string",
      "format": "uuid",
      "description": "The run this one is a retry of. Set at creation and immutable."
     },
     "retriedByRunId": {
      "type": "string",
      "format": "uuid",
      "description": "The most recent run that retried THIS one. The list and the detail read carry it."
     },
     "parentRunId": {
      "type": "string",
      "format": "uuid",
      "description": "For a run started by another run's Sub-flow step (`triggerType` `subflow`), the run that called it. The list and the detail read carry it; absent for every other run and for sub-flow runs from before 2026-10."
     },
     "restarts": {
      "type": "integer",
      "minimum": 1,
      "description": "How many times the run was offered to another worker because the one running it stopped (killed, out of memory, its host lost, or it lost the run's lease). A restarted run continues from what the stopped attempt finished: a step it finished does not run again (its step record says `recovery` `replayed`), and a step that changes something outside the run and was in flight is not sent again (`recovery` `unknown`, failed with \"Outcome unknown\"). Absent: never restarted, and every run from before 2026-11."
     },
     "answer": {
      "type": "string",
      "enum": [
       "approved",
       "rejected",
       "timed_out"
      ],
      "description": "How the run's last wait on a person was settled: approved, rejected, or `timed_out` (nobody answered before the step's timeout and the run continued without an answer). List read only; absent when the run waited on no one, or its answer was given before 2026-10."
     },
     "workflowName": {
      "type": "string",
      "description": "The workflow's name. List read only."
     },
     "workflowTriggerType": {
      "type": "string",
      "description": "The WORKFLOW's configured trigger type (as opposed to this run's `triggerType`). List read only."
     },
     "workflowTriggerPiece": {
      "type": "string",
      "description": "The PIECE the workflow starts with (`form`, `schedule`, `gmailtrigger`), as opposed to `workflowTriggerType`, which is how the platform DELIVERS it. Several apps share one type — a hosted form is delivered over a webhook — so the type alone cannot say what a person chose. Absent when the trigger has no app (manual). List read only."
     },
     "stepTotal": {
      "type": "integer",
      "description": "Number of step rows recorded for this run. List read only — use it to avoid fetching the detail just to count steps."
     },
     "stepFailed": {
      "type": "integer",
      "description": "Number of those steps with status `failed`. List read only."
     },
     "waitStep": {
      "type": "string",
      "description": "Only on a `paused` run: the definition step NAME that suspended the run."
     },
     "resumeMode": {
      "type": "string",
      "enum": [
       "time",
       "webhook"
      ],
      "description": "Only on a `paused` run: `time` = it resumes automatically at `resumeAt`; `webhook` = it waits for an external call (an approval) and will not resume on its own. An approval with a timeout is a `time` wait that is also answerable: read `waitingOn` for whether a person is waited on."
     },
     "resumeAt": {
      "type": "string",
      "format": "date-time",
      "description": "Only on a `paused` run with `resumeMode: time`: when it will resume."
     },
     "waitingOn": {
      "type": "string",
      "enum": [
       "person",
       "time"
      ],
      "description": "Only on a `paused` run: `person` = a resume link is outstanding (an approval, a webhook resume), so someone has to answer it; `time` = it carries on by itself (a Delay, or a link already answered and resuming). The list's `waiting` filter selects by this."
     },
     "claimedAt": {
      "type": "string",
      "format": "date-time",
      "description": "Only on an unfinished run a worker has taken: when it took it. A `pending` run with `claimedAt` is executing; one without it is about to be taken."
     },
     "waitingFor": {
      "type": "string",
      "enum": [
       "workflow",
       "organization"
      ],
      "description": "Only on a `waiting` run held by a runs-at-once limit: `workflow` = its workflow is at its limit (`settings.concurrencyLimit`), `organization` = its organization is (the limit set on Platform › Organizations or by the embed API). Absent when it only waits for a free worker."
     },
     "waitingLimit": {
      "type": "integer",
      "description": "Only beside `waitingFor`: the limit it waits on."
     },
     "linePosition": {
      "type": "integer",
      "minimum": 1,
      "description": "Only on a `waiting` run: its place in its workflow's line, 1 = next."
     },
     "state": {
      "type": "string",
      "enum": [
       "failed",
       "held",
       "running",
       "queued",
       "worked",
       "cancelled"
      ],
      "description": "LIST only. The run's state word: `running` covers a run in a Delay; a rejected approval is `worked` (see `decision`)."
     },
     "retryOfStartedAt": {
      "type": "string",
      "format": "date-time",
      "description": "LIST only. When the run this one retried started. Absent when it is gone."
     },
     "retryOutcome": {
      "type": "string",
      "enum": [
       "failed",
       "held",
       "running",
       "queued",
       "worked",
       "cancelled"
      ],
      "description": "LIST only. What retrying this run came to, over every retry descending from it: `worked` if any worked, else an open one's state, else the newest one's. Absent when never retried."
     },
     "retryAt": {
      "type": "string",
      "format": "date-time",
      "description": "LIST only. When the retry `retryOutcome` is about started."
     },
     "decision": {
      "type": "string",
      "enum": [
       "approved",
       "rejected",
       "timed_out"
      ],
      "description": "LIST only. How the run's last person-wait was settled (the same value as `answer`)."
     },
     "decidedBy": {
      "type": "string",
      "description": "LIST only. Who answered from the product, never an address: a member of the workspace by their name (`a member` when they gave none), `a former member` for anyone the workspace no longer has, or `token {name}`. Absent for an answer through the link itself, a timeout, one from before 2026-10-19, and to an embedded person (who reads only `decision`)."
     },
     "decidedAt": {
      "type": "string",
      "format": "date-time",
      "description": "LIST only. When the last person-wait was settled."
     },
     "heldSince": {
      "type": "string",
      "format": "date-time",
      "description": "LIST only. A held run: when its current hold began. A run that held: when its last answered hold began."
     },
     "heldMs": {
      "type": "integer",
      "format": "int64",
      "description": "LIST only. Time held on a person over every answered hold. `durationMs` includes it, so the run's own time is `durationMs - heldMs`."
     },
     "publishedVersionNumber": {
      "type": "integer",
      "description": "LIST only. The workflow's published version now. Absent for a draft."
     },
     "publishedAt": {
      "type": "string",
      "format": "date-time",
      "description": "LIST only. When that version was published."
     },
     "workflowStatus": {
      "type": "string",
      "enum": [
       "active",
       "paused",
       "draft"
      ],
      "description": "LIST only. The workflow's status now; absent when it was deleted."
     },
     "failure": {
      "type": "object",
      "description": "LIST only, failed runs. The step the error names and the first clause of the reason.",
      "required": [
       "reason"
      ],
      "properties": {
       "stepKey": {
        "type": "string",
        "description": "The step's key; absent when the message names no step."
       },
       "step": {
        "type": "string",
        "description": "Its display name in the version that ran (the draft for a test), else the key."
       },
       "reason": {
        "type": "string",
        "description": "The first clause of the reason, without the step prefix, at most 160 characters."
       }
      }
     },
     "parentWorkflowName": {
      "type": "string",
      "description": "LIST only. A sub-flow run's caller: its workflow's name."
     },
     "parentStartedAt": {
      "type": "string",
      "format": "date-time",
      "description": "LIST only. When the calling run started."
     },
     "deferred": {
      "type": "object",
      "description": "Only on a read with `large=defer`: the run's `triggerData`, when it was left out because it is over 64 KB of JSON, with its size. Read it with `GET /api/runs/{id}?step=trigger`. Absent otherwise.",
      "properties": {
       "triggerData": {
        "$ref": "#/components/schemas/DeferredValue"
       }
      }
     }
    }
   },
   "RunPagination": {
    "type": "object",
    "description": "Paging facts for a list response. Sits BESIDE `data` at the top level of the body, not inside it.",
    "required": [
     "page",
     "limit",
     "total",
     "totalPages"
    ],
    "properties": {
     "page": {
      "type": "integer",
      "description": "The page returned (1-based)."
     },
     "limit": {
      "type": "integer",
      "description": "The page size actually applied, after clamping."
     },
     "total": {
      "type": "integer",
      "description": "Total rows matching the filters, across all pages."
     },
     "totalPages": {
      "type": "integer",
      "description": "Number of pages at this `limit`."
     }
    }
   },
   "RunCancelResult": {
    "type": "object",
    "description": "The outcome of a cancel request. `cancelled: false` means nothing was done because the run had already settled.",
    "required": [
     "id",
     "status",
     "cancelled"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid",
      "description": "The run id."
     },
     "status": {
      "type": "string",
      "enum": [
       "cancelling",
       "cancelled",
       "completed",
       "failed",
       "pending",
       "running",
       "paused"
      ],
      "description": "`cancelling` = the request was signalled to the executor and the run is NOT yet terminal (poll `GET /api/runs/{id}`). `cancelled` = a paused run was settled here and now. Any other value is the status the run had ALREADY settled at, returned with `cancelled: false`."
     },
     "cancelled": {
      "type": "boolean",
      "description": "`true` when a cancellation was requested or performed; `false` when the run was already terminal and nothing happened."
     }
    }
   },
   "RunDeleteResult": {
    "type": "object",
    "required": [
     "id",
     "deleted"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid",
      "description": "The run that was deleted."
     },
     "deleted": {
      "type": "boolean",
      "description": "Always `true` on a 200."
     }
    }
   },
   "RunBulkDeleteRequest": {
    "type": "object",
    "description": "The ids to delete. Only terminal runs are removed; the rest are skipped.",
    "required": [
     "ids"
    ],
    "properties": {
     "ids": {
      "type": "array",
      "minItems": 1,
      "maxItems": 500,
      "items": {
       "type": "string",
       "format": "uuid"
      },
      "description": "Between 1 and 500 run ids. The 1-500 bound is checked on the RAW list; entries that are not valid UUIDs are then dropped silently."
     }
    }
   },
   "RunBulkDeleteResult": {
    "type": "object",
    "required": [
     "deleted",
     "requested",
     "skipped"
    ],
    "properties": {
     "deleted": {
      "type": "integer",
      "description": "Runs actually removed."
     },
     "requested": {
      "type": "integer",
      "description": "How many posted ids were parseable UUIDs, an id posted twice counted twice — NOT the number of ids you posted."
     },
     "skipped": {
      "type": "integer",
      "description": "`requested - deleted`: ids that named an in-flight/paused run, a run in another workspace, a run that no longer exists, or a repeat of an id already deleted."
     }
    }
   },
   "RunStats": {
    "type": "object",
    "description": "The caller's workspace's in-flight run counts.",
    "required": [
     "runs"
    ],
    "properties": {
     "runs": {
      "type": "object",
      "description": "Non-terminal runs in the caller's workspace. All four keys are always present.",
      "required": [
       "pending",
       "running",
       "paused",
       "waiting"
      ],
      "properties": {
       "pending": {
        "type": "integer",
        "description": "Not finished and not paused: waiting in line or executing (includes `waiting`)."
       },
       "waiting": {
        "type": "integer",
        "description": "Of `pending`, the runs waiting in line for a free slot (a runs-at-once limit or a free worker)."
       },
       "running": {
        "type": "integer",
        "description": "Currently executing."
       },
       "paused": {
        "type": "integer",
        "description": "Suspended on a durable wait."
       }
      }
     },
     "queue": {
      "type": "object",
      "deprecated": true,
      "description": "Always `{\"available\": false}`. Kept for one release for dashboards loaded before it; it will be removed.",
      "properties": {
       "available": {
        "type": "boolean",
        "const": false
       }
      }
     },
     "keel": {
      "type": "object",
      "deprecated": true,
      "description": "Always `{\"available\": false}`. Kept for one release for dashboards loaded before it; it will be removed.",
      "properties": {
       "available": {
        "type": "boolean",
        "const": false
       }
      }
     }
    }
   },
   "RunStreamEvent": {
    "type": "object",
    "description": "One JSON object delivered as the `data:` of an unnamed SSE event on `GET /api/runs/{id}/stream`. (The initial `connected` event is NOT one of these — its data is just `{\"runId\":\"...\"}`.) Progress metadata only: step output is never streamed.",
    "required": [
     "type",
     "runId",
     "status"
    ],
    "properties": {
     "type": {
      "type": "string",
      "enum": [
       "run_started",
       "run_completed",
       "run_failed",
       "run_cancelled",
       "run_paused",
       "run_resumed",
       "step_started",
       "step_completed",
       "step_failed",
       "step_retrying",
       "step_skipped"
      ],
      "description": "What happened. `run_completed`, `run_failed` and `run_cancelled` are TERMINAL — nothing follows them, and the server closes the stream after sending one. `run_paused` means the run suspended on a durable wait and may resume later (a `run_resumed` arrives on the SAME run id if it does, though a client that disconnected will have to re-subscribe). `step_failed` is sent for each failed attempt, so a step that will retry sends `step_failed` and then `step_retrying`, which is informational: the step has not failed the run yet."
     },
     "runId": {
      "type": "string",
      "format": "uuid",
      "description": "The run this event belongs to."
     },
     "nodeId": {
      "type": "string",
      "description": "For `step_*` events (and `run_paused`/`run_resumed`), the definition step NAME the event is about. Empty on whole-run events."
     },
     "status": {
      "type": "string",
      "enum": [
       "running",
       "completed",
       "failed",
       "cancelled",
       "paused",
       "skipped",
       "retrying"
      ],
      "description": "The status the subject moved to. Note `retrying` appears here but is NOT a persisted step status."
     },
     "data": {
      "type": [
       "object",
       "null"
      ],
      "description": "Event-specific metadata, never step output. `step_started`: `{attempt}` (none on a Branch or Switch). A wait's `step_completed` after a resume has none. `step_completed`: `{duration_ms, attempt}` (and `pinned: true` when frozen test data was replayed instead of executing); a branching step's (Branch or Switch) is `{duration_ms, matchedBranches, matchedBranch}`: the 0-based positions of the arms it took (empty when only Otherwise ran) and the first one's name, or `otherwise`. `step_retrying`: `{attempt, maxRetries, delay_ms}`. `run_paused`: `{waitStep, resumeMode, resumeAt?}`. No event carries a resume token."
     },
     "error": {
      "type": "string",
      "description": "The failure message on `step_failed`, `step_retrying` and `run_failed`, or the cancellation reason on `run_cancelled`. Absent otherwise."
     }
    }
   },
   "CatalogStatus": {
    "type": "object",
    "description": "Where the served app catalog came from and how healthy the fleet is.",
    "required": [
     "source",
     "degraded",
     "fingerprint",
     "pieceCount",
     "liveCatalogs"
    ],
    "properties": {
     "source": {
      "type": "string",
      "enum": [
       "fleet",
       "last-known",
       "compiled"
      ],
      "description": "`fleet`: live workers published this. `last-known`: no worker is checked in, serving the last published catalog. `compiled`: nothing has ever been published, serving the API binary's own registry."
     },
     "degraded": {
      "type": "boolean",
      "description": "True whenever `source` is not `fleet`. Flows can be read and edited, but nothing will execute until a worker returns."
     },
     "fingerprint": {
      "type": "string",
      "description": "Content hash of the catalog. Same value that prefixes the `ETag` on GET /api/pieces; changes when the fleet's catalog changes."
     },
     "pieceCount": {
      "type": "integer",
      "description": "How many apps the served catalog contains (65 in the standard build)."
     },
     "liveCatalogs": {
      "type": "integer",
      "description": "How many DISTINCT catalogs live workers reported. More than 1 means a rollout is in progress and the served catalog is the intersection of those generations."
     }
    }
   },
   "CatalogIndexEntry": {
    "type": "object",
    "description": "One row of `GET /api/pieces?view=index` — enough to choose an app and know what to fetch next. No schemas.",
    "required": [
     "id",
     "name"
    ],
    "properties": {
     "id": {
      "type": "string",
      "description": "The value a step stores in `pieceId`.",
      "example": "http"
     },
     "name": {
      "type": "string",
      "description": "Human-facing name.",
      "example": "HTTP Request"
     },
     "description": {
      "type": "string",
      "description": "One-line description, truncated at 140 characters (the full text is on the app itself)."
     },
     "category": {
      "type": "string",
      "description": "Grouping label, e.g. Data, Logic, Triggers, Communication, HTTP."
     },
     "version": {
      "type": "string",
      "description": "The version a NEW step is pinned to — the default, not necessarily the newest.",
      "example": "1.0.0"
     },
     "versions": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Every version the fleet can run, oldest first."
     },
     "actionIds": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Action ids of the DEFAULT version — the values a step stores in `actionId`."
     },
     "triggerIds": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Trigger ids of the default version. A definition's trigger stores its id in a field called `actionId`. Absent when it has none, as `actionIds` is."
     },
     "needsConnection": {
      "type": "boolean",
      "description": "True when the app declares auth: a step using it will fail at run time without a connection. Absent when false."
     }
    }
   },
   "CatalogPiece": {
    "type": "object",
    "description": "An app as the full catalog serves it, with every action and trigger schema inlined.",
    "required": [
     "id",
     "name",
     "version",
     "kind"
    ],
    "properties": {
     "id": {
      "type": "string",
      "description": "The value a step stores in `pieceId`."
     },
     "name": {
      "type": "string"
     },
     "description": {
      "type": "string"
     },
     "logoUrl": {
      "type": "string",
      "description": "Icon path. A `/icons/` prefix is what classifies the app as `core`."
     },
     "version": {
      "type": "string",
      "description": "The version a new step is pinned to (the default), not necessarily the newest in `versions`."
     },
     "versions": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Every version the fleet can run, oldest first. An unpinned step resolves to the OLDEST."
     },
     "icon": {
      "type": "string",
      "description": "Icon name for apps without a logo file."
     },
     "category": {
      "type": "string"
     },
     "nodeType": {
      "type": "string",
      "enum": [
       "action",
       "trigger",
       "branch",
       "loop"
      ],
      "description": "What kind of node this app produces on the canvas."
     },
     "kind": {
      "type": "string",
      "enum": [
       "core",
       "app"
      ],
      "description": "`core` = shipped and owned by this product; `app` = third-party connector."
     },
     "actions": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "$ref": "#/components/schemas/CatalogActionDefinition"
      },
      "description": "Actions of the DEFAULT version only. `null` when the app has none."
     },
     "triggers": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "$ref": "#/components/schemas/CatalogTriggerDefinition"
      },
      "description": "Triggers of the DEFAULT version only. `null` when the app has none."
     },
     "auth": {
      "$ref": "#/components/schemas/CatalogAuthDefinition"
     },
     "keywords": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Words people search for the app by that its name and description do not say (\"email\" for Gmail). Absent for most apps."
     },
     "actionsByVersion": {
      "type": "object",
      "additionalProperties": {
       "type": "array",
       "items": {
        "type": "string"
       }
      },
      "description": "Action ids each version declares, keyed by version. Validate a step pinned to an older `pieceVersion` against ITS entry, not against `actions`."
     },
     "actionsAt": {
      "type": "object",
      "additionalProperties": {
       "type": "array",
       "items": {
        "type": "object"
       }
      },
      "description": "The FULL action definitions (input schemas included) of every version other than the default, keyed by version; `actions` is the default's. A step pinned to an older `pieceVersion` is described by its entry here — `GET /api/pieces/{id}/actions/{actionId}?version=` answers one."
     },
     "triggersByVersion": {
      "type": "object",
      "additionalProperties": {
       "type": "array",
       "items": {
        "type": "string"
       }
      },
      "description": "Trigger ids each version declares, keyed by version."
     }
    }
   },
   "CatalogActionDefinition": {
    "type": "object",
    "description": "One action as the full catalog serves it.",
    "required": [
     "id",
     "name",
     "inputSchema",
     "outputSchema"
    ],
    "properties": {
     "id": {
      "type": "string",
      "description": "The value a step stores in `actionId`."
     },
     "name": {
      "type": "string"
     },
     "description": {
      "type": "string"
     },
     "sideEffects": {
      "type": "string",
      "enum": [
       "pure",
       "reads_external",
       "writes_external"
      ],
      "description": "What running this does to the world. An untagged action is treated as `writes_external` (fail-safe). Anything that can write must not be test-executed without the user's consent."
     },
     "inputSchema": {
      "$ref": "#/components/schemas/CatalogJsonSchema"
     },
     "outputSchema": {
      "$ref": "#/components/schemas/CatalogJsonSchema"
     },
     "sampleData": {
      "type": "object",
      "additionalProperties": true,
      "description": "Structurally accurate example of this action's output."
     },
     "errorHandling": {
      "type": "object",
      "description": "Per-action retry defaults.",
      "properties": {
       "retryOnFailure": {
        "type": "boolean"
       },
       "maxRetries": {
        "type": "integer"
       },
       "continueOnFailure": {
        "type": "boolean"
       }
      }
     },
     "timeoutSeconds": {
      "type": "integer",
      "description": "Budget this action needs when the step sets none. 0 means the engine default (30s)."
     }
    }
   },
   "CatalogTriggerDefinition": {
    "type": "object",
    "description": "One trigger as the full catalog serves it.",
    "required": [
     "id",
     "name",
     "type",
     "inputSchema",
     "outputSchema"
    ],
    "properties": {
     "id": {
      "type": "string",
      "description": "The value a definition's trigger stores in its `actionId` field."
     },
     "name": {
      "type": "string"
     },
     "description": {
      "type": "string"
     },
     "type": {
      "type": "string",
      "enum": [
       "manual",
       "webhook",
       "cron",
       "poll",
       "error",
       "run_finished",
       "callable"
      ],
      "description": "How this trigger fires. ALWAYS LOWERCASE — unlike a step's `type` (PIECE / BRANCH / LOOP / CODE), which is uppercase."
     },
     "inputSchema": {
      "$ref": "#/components/schemas/CatalogJsonSchema"
     },
     "outputSchema": {
      "$ref": "#/components/schemas/CatalogJsonSchema"
     },
     "sampleData": {
      "type": "object",
      "additionalProperties": true,
      "description": "Declared example of what `{{trigger.*}}` resolves to."
     },
     "derivesSample": {
      "type": "boolean",
      "description": "True when the real payload shape depends on this trigger's configuration, so `sampleData` is generic and misleading. Call POST /api/pieces/{id}/triggers/{triggerId}/sample instead."
     }
    }
   },
   "CatalogAuthDefinition": {
    "type": "object",
    "description": "The credential an app needs. Present means a connection is required before the app can run.",
    "required": [
     "type",
     "required"
    ],
    "properties": {
     "type": {
      "type": "string",
      "enum": [
       "none",
       "api_key",
       "bearer_token",
       "basic_auth",
       "oauth2",
       "custom"
      ],
      "description": "Credential family."
     },
     "required": {
      "type": "boolean"
     },
     "description": {
      "type": "string"
     },
     "fields": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/CatalogAuthField"
      }
     },
     "oauth2": {
      "$ref": "#/components/schemas/CatalogOAuth2Config"
     },
     "connectionPiece": {
      "type": "string",
      "description": "The app whose connections this one uses (a poll trigger and its action sibling: `gmailtrigger` → `gmail`). A connection created for this app is created under that one, and serves both."
     },
     "helpUrl": {
      "type": "string",
      "format": "uri",
      "description": "Where to make the credential at the service."
     }
    }
   },
   "CatalogAuthField": {
    "type": "object",
    "description": "One field of a credential form.",
    "required": [
     "name",
     "title",
     "type",
     "required"
    ],
    "properties": {
     "name": {
      "type": "string"
     },
     "title": {
      "type": "string"
     },
     "type": {
      "type": "string",
      "enum": [
       "string",
       "password",
       "boolean"
      ]
     },
     "required": {
      "type": "boolean"
     },
     "description": {
      "type": "string"
     },
     "standalone": {
      "type": "boolean",
      "description": "On an app that also signs in with OAuth2: the credential a person makes in the service (a personal access token, an API key, a bot or app token) that works on its own and lasts. A form that cannot offer the OAuth sign-in asks for it alone. Absent means false."
     }
    }
   },
   "CatalogOAuth2Config": {
    "type": "object",
    "description": "The OAuth2 endpoints an app declares. These are the trusted endpoints; a managed instance app is keyed off `authorizationUrl`.",
    "required": [
     "authorizationUrl",
     "tokenUrl"
    ],
    "properties": {
     "authorizationUrl": {
      "type": "string"
     },
     "tokenUrl": {
      "type": "string"
     },
     "scopes": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "scopeSep": {
      "type": "string",
      "description": "Delimiter used to join scopes. Defaults to a single space; a few providers want a comma."
     },
     "authParams": {
      "type": "object",
      "additionalProperties": {
       "type": "string"
      },
      "description": "Extra query parameters merged into the authorize URL."
     }
    }
   },
   "CatalogJsonSchema": {
    "type": "object",
    "description": "An operation's full input or output schema, as served by the default catalog views.",
    "properties": {
     "type": {
      "type": "string",
      "example": "object"
     },
     "properties": {
      "type": "object",
      "additionalProperties": {
       "$ref": "#/components/schemas/CatalogSchemaProperty"
      }
     },
     "required": {
      "type": "array",
      "items": {
       "type": "string"
      }
     }
    }
   },
   "CatalogSchemaProperty": {
    "type": "object",
    "description": "One property of a full (unprojected) schema. Recursive through `properties` and `items`. Carries presentation fields (`title`, `order`, `placeholder`, `group`) that the projected views drop.",
    "properties": {
     "type": {
      "type": "string",
      "description": "JSON type: string, number, boolean, object, array."
     },
     "title": {
      "type": "string"
     },
     "description": {
      "type": "string",
      "description": "The field's help as the step panel shows it."
     },
     "default": {
      "description": "Default value, of any JSON type."
     },
     "enum": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Closed set of legal values."
     },
     "enumLabels": {
      "type": "object",
      "additionalProperties": {
       "type": "string"
      },
      "description": "Display labels for the enum values."
     },
     "format": {
      "type": "string"
     },
     "properties": {
      "type": "object",
      "additionalProperties": {
       "$ref": "#/components/schemas/CatalogSchemaProperty"
      }
     },
     "items": {
      "$ref": "#/components/schemas/CatalogSchemaProperty"
     },
     "propertyType": {
      "type": "string",
      "description": "How the value is chosen: SHORT_TEXT, LONG_TEXT, STATIC_DROPDOWN, DROPDOWN, MULTI_SELECT, DYNAMIC, JSON, CODE, CHECKBOX, NUMBER, SECRET_TEXT and similar. DROPDOWN / MULTI_SELECT / DYNAMIC mean the legal values come from POST .../options/{propertyName} at run time, not from this schema."
     },
     "placeholder": {
      "type": "string"
     },
     "group": {
      "type": "string"
     },
     "order": {
      "type": "integer"
     },
     "required": {
      "type": "boolean"
     },
     "minLength": {
      "type": "integer"
     },
     "maxLength": {
      "type": "integer"
     },
     "min": {
      "type": "number"
     },
     "max": {
      "type": "number"
     },
     "pattern": {
      "type": "string"
     },
     "refreshers": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Other property names whose change invalidates this property's resolved options."
     },
     "refreshOnSearch": {
      "type": "boolean"
     },
     "expressionEnabled": {
      "type": "boolean"
     },
     "language": {
      "type": "string",
      "description": "For CODE properties: the language of the editor, e.g. javascript."
     },
     "displayConditions": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/CatalogDisplayCondition"
      },
      "description": "ANDed conditions deciding whether this property applies. The fields they name are this operation's gate fields — the ones you can pass as query parameters to narrow the operation detail."
     },
     "bound": {
      "type": "boolean",
      "description": "The value, though typed as text, points into the connected account (a spreadsheet, a calendar, a folder, a label): it means nothing to another account, so a template used outside the workspace it was saved in arrives with it blank. Absent when false."
     },
     "noExpressions": {
      "type": "boolean",
      "description": "True when `{{ }}` is NOT replaced in this field: the value is used exactly as written (Code 2.0.0's `code`, where values come from `inputs`, `steps` and `trigger` instead). Never write an expression into such a field."
     },
     "agentNotes": {
      "type": "string",
      "description": "What an agent writing this field must know beyond `description`; the step panel does not show it. The projected views append it to `description`. Absent when there is none."
     }
    }
   },
   "CatalogDisplayCondition": {
    "type": "object",
    "description": "One visibility rule. All conditions on a property are ANDed.",
    "required": [
     "field",
     "operator"
    ],
    "properties": {
     "field": {
      "type": "string",
      "description": "The sibling property whose value decides this one. Every gate field is itself a property of the same schema, so its declared type and enum are the legal values."
     },
     "operator": {
      "type": "string",
      "enum": [
       "eq",
       "neq",
       "in",
       "notIn",
       "contains_any",
       "exists",
       "notExists"
      ],
      "description": "An unrecognized operator is treated permissively (the property is kept)."
     },
     "value": {
      "description": "Comparison value for eq/neq, of any scalar JSON type."
     },
     "values": {
      "type": "array",
      "items": {},
      "description": "Candidate list for in / notIn / contains_any."
     }
    }
   },
   "CatalogPieceSummary": {
    "type": "object",
    "description": "`GET /api/pieces/{id}?view=summary` — every operation of one app with no schemas at all.",
    "required": [
     "id",
     "name",
     "actions",
     "triggers"
    ],
    "properties": {
     "id": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "description": {
      "type": "string"
     },
     "category": {
      "type": "string"
     },
     "version": {
      "type": "string"
     },
     "versions": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "needsConnection": {
      "type": "boolean",
      "description": "True when the app declares auth."
     },
     "actions": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/CatalogOperationSummary"
      }
     },
     "triggers": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/CatalogOperationSummary"
      }
     }
    }
   },
   "CatalogOperationSummary": {
    "type": "object",
    "description": "One action or trigger without its schema — enough to pick between them.",
    "required": [
     "id",
     "fields"
    ],
    "properties": {
     "id": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "description": {
      "type": "string"
     },
     "sideEffects": {
      "type": "string",
      "enum": [
       "pure",
       "reads_external",
       "writes_external"
      ],
      "description": "Actions only; always absent for triggers."
     },
     "fields": {
      "type": "integer",
      "description": "How many input properties this operation declares — a one-field action versus a forty-field one, before fetching either."
     },
     "discriminators": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/CatalogDiscriminator"
      },
      "description": "The gate fields this operation's inputs depend on, MOST FAR-REACHING FIRST. These are exactly the query parameters worth supplying to the operation-detail endpoint."
     }
    }
   },
   "CatalogDiscriminator": {
    "type": "object",
    "description": "A gate field plus the values it accepts.",
    "required": [
     "field"
    ],
    "properties": {
     "field": {
      "type": "string",
      "description": "Property name — send it verbatim as a query parameter, e.g. `?auth_type=BASIC`."
     },
     "type": {
      "type": "string",
      "description": "The field's declared type (string, boolean, number). Query values are text and are coerced to this type."
     },
     "values": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "The field's enum, when it declares one — the legal values for the query parameter."
     }
    }
   },
   "CatalogOperationDetail": {
    "type": "object",
    "description": "One action or trigger with its schema, narrowed to the fields that apply given the gate values supplied in the query string.",
    "required": [
     "pieceId",
     "id",
     "inputSchema"
    ],
    "properties": {
     "pieceId": {
      "type": "string"
     },
     "pieceVersion": {
      "type": "string",
      "description": "The app's default version — what a new step referencing this operation will be pinned to."
     },
     "id": {
      "type": "string",
      "description": "The operation id: a step's `actionId`, or a trigger's `actionId`."
     },
     "name": {
      "type": "string"
     },
     "description": {
      "type": "string"
     },
     "sideEffects": {
      "type": "string",
      "enum": [
       "pure",
       "reads_external",
       "writes_external"
      ],
      "description": "Actions only; always empty on the trigger endpoint."
     },
     "inputSchema": {
      "$ref": "#/components/schemas/CatalogProjectedSchema"
     },
     "outputSchema": {
      "$ref": "#/components/schemas/CatalogProjectedSchema"
     },
     "sampleData": {
      "type": "object",
      "additionalProperties": true,
      "description": "Declared example output (for a trigger, the declared `{{trigger.*}}` shape)."
     },
     "filteredBy": {
      "type": "object",
      "additionalProperties": {
       "type": "string"
      },
      "description": "The gate values that were ACTUALLY applied — the raw query intersected with the fields this operation is really gated on. If a parameter you sent is missing here, it narrowed nothing. Absent when nothing narrowed."
     },
     "ignored": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Query keys that gate nothing on this operation — a misspelling (`bodyType` for `body_type`), or a gate belonging to a different operation. Fix these rather than believing the filter worked. Absent when empty."
     },
     "unresolved": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Gate fields the SURVIVING properties still depend on and which you did not supply. Adding these as query parameters narrows the schema further."
     },
     "conditionalFields": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Fields kept ONLY because their gate was left undecided. They are deliberately excluded from `required` even when the app declares them required — supply their gate to find out whether they really apply."
     },
     "dynamicFields": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Properties whose legal values are not in this schema. Fetch them from POST /api/pieces/{id}/actions/{actionId}/options/{propertyName}."
     }
    }
   },
   "CatalogProjectedSchema": {
    "type": "object",
    "description": "A schema as the projections serve it: presentation fields removed, properties possibly narrowed.",
    "required": [
     "properties",
     "required"
    ],
    "properties": {
     "type": {
      "type": "string"
     },
     "properties": {
      "type": "object",
      "additionalProperties": {
       "$ref": "#/components/schemas/CatalogProjectedProperty"
      }
     },
     "required": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Always present (possibly empty). Excludes every field listed in `conditionalFields`."
     }
    }
   },
   "CatalogProjectedProperty": {
    "type": "object",
    "description": "One property as the projections serve it: everything that constrains a legal value, nothing about screen layout (`title`, `order`, `placeholder` and `group` are dropped — 11.8% of the payload). Recursive through `properties` and `items`.",
    "properties": {
     "type": {
      "type": "string"
     },
     "description": {
      "type": "string",
      "description": "The field's whole help for whoever writes it: the step panel's text, then the notes for an agent (`agentNotes` in the full schema)."
     },
     "default": {
      "description": "Default value, of any JSON type."
     },
     "enum": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Closed set of legal values."
     },
     "enumLabels": {
      "type": "object",
      "additionalProperties": {
       "type": "string"
      },
      "description": "Display labels; omitted entirely when they are identical to the enum values."
     },
     "format": {
      "type": "string"
     },
     "properties": {
      "type": "object",
      "additionalProperties": {
       "$ref": "#/components/schemas/CatalogProjectedProperty"
      }
     },
     "items": {
      "$ref": "#/components/schemas/CatalogProjectedProperty"
     },
     "propertyType": {
      "type": "string",
      "description": "How the value is chosen. DROPDOWN / MULTI_SELECT / DYNAMIC mean the options come from the app at run time — see `dynamicFields`."
     },
     "required": {
      "type": "boolean",
      "description": "The per-property requiredness flag. The schema-level `required` array is authoritative for the narrowed view."
     },
     "minLength": {
      "type": "integer"
     },
     "maxLength": {
      "type": "integer"
     },
     "min": {
      "type": "number"
     },
     "max": {
      "type": "number"
     },
     "pattern": {
      "type": "string"
     },
     "refreshers": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "language": {
      "type": "string"
     },
     "displayConditions": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/CatalogDisplayCondition"
      },
      "description": "Kept deliberately: a field marked conditional must show WHAT would decide it."
     },
     "bound": {
      "type": "boolean",
      "description": "The value, though typed as text, points into the connected account (a spreadsheet, a calendar, a folder, a label): it means nothing to another account, so a template used outside the workspace it was saved in arrives with it blank. Absent when false."
     },
     "noExpressions": {
      "type": "boolean",
      "description": "True when `{{ }}` is NOT replaced in this field: the value is used exactly as written (Code 2.0.0's `code`, where values come from `inputs`, `steps` and `trigger` instead). Never write an expression into such a field."
     }
    }
   },
   "PieceDynamicPropertiesRequest": {
    "type": "object",
    "description": "Context for resolving a property's dynamic sub-schema.",
    "properties": {
     "values": {
      "type": "object",
      "additionalProperties": true,
      "description": "The step's configuration so far, keyed by property name — the same object that becomes the step's `config`. For the sub-flow case put the target workflow's UUID in `values.workflow_id`."
     },
     "connectionId": {
      "type": "string",
      "format": "uuid",
      "description": "Connection whose credentials the app should use. Only the id travels; the worker decrypts it."
     }
    }
   },
   "PieceDynamicPropertiesResponse": {
    "type": "object",
    "description": "The resolved sub-schema.",
    "required": [
     "properties"
    ],
    "properties": {
     "properties": {
      "type": "object",
      "additionalProperties": {
       "$ref": "#/components/schemas/CatalogSchemaProperty"
      },
      "description": "Resolved properties keyed by property name, in the full (unprojected) property shape."
     }
    }
   },
   "PieceDynamicOptionsRequest": {
    "type": "object",
    "description": "Context for resolving a dropdown's options.",
    "properties": {
     "values": {
      "type": "object",
      "additionalProperties": true,
      "description": "The step's configuration so far. Options often depend on an earlier field (see the property's `refreshers`)."
     },
     "connectionId": {
      "type": "string",
      "format": "uuid",
      "description": "Connection whose credentials the app should use when it must call the third party."
     },
     "searchQuery": {
      "type": "string",
      "description": "Server-side filter term, honoured only by apps whose property declares `refreshOnSearch`."
     }
    }
   },
   "PieceDropdownOptions": {
    "type": "object",
    "description": "The legal values for a dynamic dropdown.",
    "required": [
     "options"
    ],
    "properties": {
     "options": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "type": "object",
       "required": [
        "label",
        "value"
       ],
       "properties": {
        "label": {
         "type": "string",
         "description": "Display text only — never store this in a step's config."
        },
        "value": {
         "type": "string",
         "description": "The value to write into the step's `config` for this property."
        }
       }
      },
      "description": "The choices. `null` when the app has none to offer yet (no connection, an earlier field unset); `placeholder` then says what to do."
     },
     "disabled": {
      "type": "boolean",
      "description": "True when the app could not offer choices (for example a prerequisite field is unset)."
     },
     "placeholder": {
      "type": "string",
      "description": "Hint text, e.g. `Select a workflow`."
     }
    }
   },
   "PieceTriggerSampleRequest": {
    "type": "object",
    "description": "The trigger configuration to derive a sample for. Optional in full.",
    "properties": {
     "config": {
      "type": "object",
      "additionalProperties": true,
      "description": "The trigger's `config` object as it would sit in the definition. Absent or empty means 'no config yet' and the declared fallback is returned."
     }
    }
   },
   "PieceTriggerSampleResponse": {
    "type": "object",
    "description": "What `{{trigger.*}}` will contain.",
    "required": [
     "sampleData",
     "derived"
    ],
    "properties": {
     "sampleData": {
      "type": "object",
      "additionalProperties": true,
      "description": "The payload shape. Reference its keys from steps as `{{trigger.<key>}}` (e.g. `{{trigger.body.email}}` for a webhook)."
     },
     "derived": {
      "type": "boolean",
      "description": "True when this reflects the configuration you sent; false when it is the trigger's generic declaration. A generic sample that looks authoritative is how authors build against a shape that never arrives."
     }
    }
   },
   "ScheduleNextRunsRequest": {
    "type": "object",
    "description": "The schedule trigger's configuration to preview.",
    "properties": {
     "config": {
      "type": "object",
      "additionalProperties": true,
      "description": "The schedule trigger's `config`, e.g. `{\"frequency\":\"every_day\",\"hour_of_the_day\":9,\"timezone\":\"UTC\",\"run_on_weekends\":true}`. Absent previews the app's defaults. Legal fields per frequency come from GET /api/pieces/schedule/triggers/schedule?frequency=<value>."
     }
    }
   },
   "ScheduleNextRunsResponse": {
    "type": "object",
    "description": "The compiled schedule. Either `nextRuns` or `error` is present, never both.",
    "properties": {
     "cron": {
      "type": "string",
      "description": "The cron expression this configuration compiles to. Empty when the frequency or the cron is unusable.",
      "example": "0 0 * * 1-5"
     },
     "timezone": {
      "type": "string",
      "description": "The zone the fire times are computed in. Empty when the frequency or the cron is unusable; an unknown zone is answered as sent, beside `error`.",
      "example": "UTC"
     },
     "nextRuns": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "date-time"
      },
      "description": "The next 5 fire times, RFC 3339 in the schedule's own zone (`2026-10-31T09:00:00+01:00`; `Z` for UTC). Absent when `error` is set. The count is fixed and cannot be requested."
     },
     "error": {
      "type": "string",
      "description": "Why this schedule can never fire — an invalid cron, an unknown frequency, an unknown time zone. Carried in a 200 body, not an HTTP error.",
      "example": "unknown frequency: custom_cron"
     }
    }
   },
   "WfPagination": {
    "type": "object",
    "description": "Paging envelope returned alongside `data` on list endpoints.",
    "properties": {
     "page": {
      "type": "integer",
      "description": "The 1-based page that was returned."
     },
     "limit": {
      "type": "integer",
      "description": "Rows per page actually applied (not necessarily what you asked for — out-of-range values fall back to 20)."
     },
     "total": {
      "type": "integer",
      "description": "Total matching rows across all pages."
     },
     "totalPages": {
      "type": "integer"
     }
    },
    "required": [
     "page",
     "limit",
     "total",
     "totalPages"
    ]
   },
   "WfWorkflow": {
    "type": "object",
    "description": "A workflow. `definition` is the DRAFT — what the editor saves into. Production executes the snapshot named by `latestVersionId` instead, which is why `hasUnpublishedChanges` matters.",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string"
     },
     "description": {
      "type": "string"
     },
     "definition": {
      "$ref": "#/components/schemas/WfFlowDefinition"
     },
     "status": {
      "type": "string",
      "enum": [
       "draft",
       "active",
       "paused"
      ],
      "description": "`draft` = never published. `active` = live, triggers registered, public webhook reachable. `paused` = was live, toggled off."
     },
     "triggerType": {
      "type": "string",
      "enum": [
       "manual",
       "cron",
       "webhook",
       "error",
       "run_finished",
       "poll"
      ],
      "description": "Derived from the definition's trigger on every write. Only `webhook` workflows have a `webhookToken`."
     },
     "cronExpression": {
      "type": [
       "string",
       "null"
      ]
     },
     "webhookToken": {
      "type": [
       "string",
       "null"
      ],
      "description": "The workflow's public production door. `POST /api/webhooks/{webhookToken}` is UNAUTHENTICATED — the unguessable URL is the credential. Minted by publish for a webhook workflow, revoked and replaced by `POST /api/workflows/{id}/webhook/rotate`. Present here only for a credential holding `workflow:run`; redacted below that, and never present in the workflow LIST."
     },
     "latestVersionId": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "The published snapshot production runs. Null until first publish."
     },
     "definitionRev": {
      "type": "integer",
      "description": "Optimistic-lock revision, bumped on every save. Pass it back as `expectedRev` to make a write conditional. Present on the single-workflow read and on write responses; absent (0) in lists."
     },
     "hasUnpublishedChanges": {
      "type": "boolean",
      "description": "True when the draft has moved ahead of what is live. Computed in SQL, so it is exact — undoing an edit back to the published bytes reads false again."
     },
     "folderId": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "tags": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "testPayloads": {
      "type": [
       "object",
       "null"
      ],
      "additionalProperties": true,
      "description": "Saved test fixtures. Stored outside the definition, so publish/restore never resets them."
     },
     "pinnedData": {
      "type": [
       "object",
       "null"
      ],
      "additionalProperties": true,
      "description": "Frozen step outputs replayed instead of executing, TEST runs only. Stored outside the definition."
     },
     "workspaceId": {
      "type": "string",
      "format": "uuid"
     },
     "orgId": {
      "type": "string",
      "format": "uuid"
     },
     "createdBy": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid"
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "updatedAt": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "id",
     "name",
     "definition",
     "status",
     "triggerType",
     "workspaceId",
     "orgId",
     "createdAt",
     "updatedAt"
    ]
   },
   "WfFlowDefinition": {
    "type": "object",
    "description": "The workflow definition. It is a TREE, not a list of steps: the trigger holds `nextAction`, each action holds the next one, and branches/loops/error paths nest their own sub-chains. A flat `actions` array is not a definition and would execute nothing.",
    "properties": {
     "schemaVersion": {
      "type": "integer",
      "const": 2,
      "description": "Always 2. There is no v1 flat-graph format."
     },
     "trigger": {
      "$ref": "#/components/schemas/WfFlowTrigger"
     },
     "settings": {
      "$ref": "#/components/schemas/WfWorkflowSettings"
     },
     "variables": {
      "type": "object",
      "additionalProperties": true,
      "description": "Plain (non-secret) workflow variables, read in expressions as `{{ $vars.NAME }}`. Sensitive variables are NOT here — they live encrypted on the workflow row (see the variables endpoints)."
     },
     "annotations": {
      "description": "Canvas sticky-note metadata. Opaque to the API."
     },
     "pinnedData": {
      "type": "object",
      "additionalProperties": true
     }
    },
    "required": [
     "schemaVersion",
     "trigger"
    ]
   },
   "WfFlowTrigger": {
    "type": "object",
    "description": "The root of the tree. Note the two traps: its `type` is LOWERCASE (`webhook`, `cron`, …) while every action's `type` is UPPERCASE (`PIECE`, `BRANCH`, …); and the trigger names the thing it fires on in a field called `actionId`, not `triggerId`.",
    "properties": {
     "name": {
      "type": "string",
      "description": "Stable name. This is what `{{trigger.…}}` and the step-data endpoints address."
     },
     "displayName": {
      "type": "string"
     },
     "type": {
      "type": "string",
      "enum": [
       "manual",
       "cron",
       "webhook",
       "error",
       "run_finished",
       "poll"
      ],
      "description": "LOWERCASE — unlike action types."
     },
     "pieceId": {
      "type": "string"
     },
     "pieceVersion": {
      "type": "string",
      "description": "Pins which version of the app runs. Empty, at run time, means the OLDEST version; a write stamps it (a new step gets the app's default version, one the draft already had keeps the oldest), never the newest. A pin the live fleet cannot serve is refused, never substituted."
     },
     "actionId": {
      "type": "string",
      "description": "The trigger's id WITHIN the app. Yes — a trigger names its trigger in a field called `actionId`."
     },
     "config": {
      "type": "object",
      "additionalProperties": true,
      "description": "The trigger's configuration. It is `config`, never `settings`."
     },
     "connectionId": {
      "type": [
       "string",
       "null"
      ]
     },
     "valid": {
      "type": "boolean",
      "description": "Server-derived: \"an app and action were chosen\", not \"the config passes its schema\". A value sent is not kept."
     },
     "nextAction": {
      "$ref": "#/components/schemas/WfFlowAction"
     }
    },
    "required": [
     "name",
     "type",
     "config",
     "valid"
    ]
   },
   "WfFlowAction": {
    "type": "object",
    "description": "One step. Recursive: `nextAction` continues the chain, `branches[].firstAction` heads a branch arm, `firstLoopAction` heads a loop body, `onErrorAction` heads the error path.",
    "properties": {
     "name": {
      "type": "string",
      "description": "Stable name, unique in the workflow. Expressions address it as `{{steps.<name>.…}}`, run detail keys step output by it (`steps[].nodeId`), and `fromNode`/`step`/`nodeId` parameters all take it."
     },
     "displayName": {
      "type": "string"
     },
     "type": {
      "type": "string",
      "enum": [
       "PIECE",
       "BRANCH",
       "LOOP",
       "CODE"
      ],
      "description": "UPPERCASE — unlike the trigger's lowercase `type`."
     },
     "pieceId": {
      "type": "string",
      "description": "The app that runs the step. A BRANCH names `branch` (or `switch`), a LOOP `loop`, a CODE step `code`; the server fills in a BRANCH's or LOOP's when it is left out."
     },
     "pieceVersion": {
      "type": "string",
      "description": "Pins which version of the app runs. Empty, at run time, means the OLDEST version; a write stamps it (a new step gets the app's default version, one the draft already had keeps the oldest)."
     },
     "actionId": {
      "type": "string",
      "description": "The action's id within the app: `evaluate` on a BRANCH, `loop_on_items` on a LOOP."
     },
     "config": {
      "type": "object",
      "additionalProperties": true,
      "description": "The step's configuration. The field is `config` — NOT `settings`. A step whose configuration arrives under `settings` loses it entirely and the app runs its defaults, which is silently wrong rather than an error."
     },
     "connectionId": {
      "type": [
       "string",
       "null"
      ]
     },
     "valid": {
      "type": "boolean",
      "description": "Server-derived: true when the step's app and action are chosen (a BRANCH, LOOP or CODE step always is). A value sent is not kept."
     },
     "skip": {
      "type": "boolean",
      "description": "Skip only this step; its `nextAction` chain still runs."
     },
     "nextAction": {
      "$ref": "#/components/schemas/WfFlowAction"
     },
     "branches": {
      "type": "array",
      "description": "BRANCH only. A run evaluates `config.branches` when it lists arms and then runs the arm at the same position here, so the two must agree; leave `config.branches` out and the server mirrors these arms into it.",
      "items": {
       "$ref": "#/components/schemas/WfFlowBranch"
      }
     },
     "firstLoopAction": {
      "$ref": "#/components/schemas/WfFlowAction"
     },
     "onErrorAction": {
      "$ref": "#/components/schemas/WfFlowAction",
      "description": "Runs when THIS step fails and `config.onError` is `errorPath`. The failure is seeded as this step's output (`{{steps.<name>.error}}`), and the normal `nextAction` continuation does NOT run."
     }
    },
    "required": [
     "name",
     "type",
     "config",
     "valid"
    ]
   },
   "WfFlowBranch": {
    "type": "object",
    "description": "One arm of a BRANCH step.",
    "properties": {
     "name": {
      "type": "string"
     },
     "conditions": {
      "type": "array",
      "description": "Nested arrays: the OUTER array is OR groups, the INNER array is ANDed conditions within a group.",
      "items": {
       "type": "array",
       "items": {
        "$ref": "#/components/schemas/WfFlowCondition"
       }
      }
     },
     "firstAction": {
      "$ref": "#/components/schemas/WfFlowAction"
     }
    },
    "required": [
     "name",
     "conditions"
    ]
   },
   "WfFlowCondition": {
    "type": "object",
    "description": "One comparison. `operator` is a closed set defined by the engine (e.g. `num_gt`, `text_contains`, `date_before`, `bool_is`, `list_is_empty`) — an invented operator such as `greaterThan` fails validation. Query the admissible set from the validation endpoint rather than guessing.",
    "properties": {
     "field": {
      "type": "string"
     },
     "operator": {
      "type": "string"
     },
     "value": {}
    },
    "required": [
     "field",
     "operator"
    ]
   },
   "WfWorkflowSettings": {
    "type": "object",
    "properties": {
     "timeoutSeconds": {
      "type": "integer",
      "description": "Caps a whole run. 0 = server default."
     },
     "errorWorkflowId": {
      "type": "string",
      "description": "Run this workflow whenever a production run of THIS one fails — never for a test from the editor, nor for a run called as a Sub-flow (its caller's setting applies). Publish refuses a value that could never fire (deleted, cross-tenant, or unpublished)."
     },
     "concurrencyLimit": {
      "type": "integer",
      "description": "Maximum production runs of this workflow executing or held (paused) at once. 0 = unlimited. A run over it WAITS in line and starts in order (status `waiting`), unless `whenFull` is `refuse`."
     },
     "whenFull": {
      "type": "string",
      "enum": [
       "wait",
       "refuse"
      ],
      "description": "What a production run does when the workflow is at `concurrencyLimit`: `wait` (the default, also when absent) waits in line; `refuse` keeps the behaviour before v2026.10.4 — a webhook or API caller gets 429 `CONCURRENCY_LIMIT`, a hosted form says it is at capacity, a schedule skips its tick and a polling trigger keeps the items for its next poll."
     },
     "maintenanceMode": {
      "type": "boolean"
     },
     "autoPublishFixes": {
      "type": "boolean"
     }
    }
   },
   "WfWorkflowRun": {
    "type": "object",
    "description": "A run. From `POST /api/workflows/{id}/run` it arrives `pending` with nothing executed — poll `GET /api/runs/{id}` for the outcome and per-step detail.",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid",
      "description": "The run id. This is what a 202 gives you to poll or stream."
     },
     "workflowId": {
      "type": "string",
      "format": "uuid"
     },
     "workspaceId": {
      "type": "string",
      "format": "uuid"
     },
     "orgId": {
      "type": "string",
      "format": "uuid"
     },
     "status": {
      "type": "string",
      "enum": [
       "pending",
       "running",
       "completed",
       "failed",
       "cancelled",
       "paused",
       "waiting"
      ],
      "description": "`pending` = queued, nothing has executed. `waiting` (with `?wait=`) = accepted and still waiting in line for a free slot after the wait: a runs-at-once limit holds it, and it starts in order. `paused` = suspended on a durable wait (delay/approval) and will continue — it is in flight, not finished."
     },
     "triggerType": {
      "type": "string",
      "description": "How the run started. `api` = a production run through `POST /api/workflows/{id}/run`; `mcp` = the same door, by an AI app's tool. `manual` = a TEST run through that same endpoint (kept as `manual` deliberately — the builder's step-data picker matches it literally). Also `webhook` (a hosted form's submission too), `cron`, `poll`, `error`, `run_finished`, `subflow`.",
      "examples": [
       "api",
       "manual",
       "webhook",
       "cron"
      ]
     },
     "triggerData": {
      "description": "The trigger payload this run received, in delivery shape (for a webhook: `method`/`headers`/`query`/`body`). This is exactly what `{{trigger.…}}` resolves against, and what you would replay as a `__shaped` body."
     },
     "startedAt": {
      "type": "string",
      "format": "date-time",
      "description": "When the run was accepted. It may then wait in line for a free slot before a worker starts it (`firstStartedAt`)."
     },
     "firstStartedAt": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "When a worker first started the run. `durationMs` counts from here, so the time from `startedAt` to this is the time the run waited in line. Null until a worker starts the run, and for a run started on v2026.10.5 or earlier or one that ran inline (a Sub-flow's, an error handler's), whose `durationMs` counts from `startedAt`."
     },
     "finishedAt": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "Absent while the run is in flight."
     },
     "durationMs": {
      "type": [
       "integer",
       "null"
      ],
      "description": "How long the run took, in milliseconds: from the first time a worker started the run (`firstStartedAt`, else `startedAt`) to its finish. Absent while the run is in flight."
     },
     "errorMessage": {
      "type": "string"
     },
     "tags": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "type": "string"
      },
      "description": "`null` on the 202 of a run just accepted, `[]` when a finished run has none."
     },
     "environment": {
      "type": "string",
      "enum": [
       "test",
       "production"
      ],
      "description": "Deliberately independent of `triggerType` — a listen-captured delivery is honestly a webhook run AND honestly a test. `GET /api/runs` defaults to production only; this workflow-scoped list returns both."
     },
     "versionId": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "The latest published version at the moment the run started — NOT necessarily what executed."
     },
     "versionNumber": {
      "type": [
       "integer",
       "null"
      ]
     },
     "executedVersionId": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "What ACTUALLY executed. Neither answer of this shape carries it, whatever ran (the 202 without `?wait=`, `GET /api/workflows/{id}/runs`): read `GET /api/runs/{id}`, where absent means the DRAFT ran, so rendering `versionId`'s snapshot would show a graph that never ran."
     },
     "executedVersionNumber": {
      "type": [
       "integer",
       "null"
      ],
      "description": "Display number of `executedVersionId`; carried only where it is."
     },
     "startedFromStep": {
      "type": [
       "string",
       "null"
      ],
      "description": "The step a PARTIAL (`fromNode`) run began at. Empty/absent means the run started at its trigger, which is every ordinary run — without it the trigger would be marked complete on a run that never executed it."
     },
     "retryOfRunId": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Lineage only, set from `?retryOf=`."
     },
     "steps": {
      "type": "array",
      "description": "Per-step results. NOT populated by the run list or by the 202 from the run endpoint — fetch `GET /api/runs/{id}`. Each entry's `outputData` is that step's output, keyed to the step by `nodeId`.",
      "items": {
       "type": "object",
       "additionalProperties": true
      }
     }
    },
    "required": [
     "id",
     "workflowId",
     "status",
     "triggerType",
     "startedAt",
     "environment"
    ]
   },
   "WfWorkflowVersion": {
    "type": "object",
    "description": "An immutable published snapshot.",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "workflowId": {
      "type": "string",
      "format": "uuid"
     },
     "versionNumber": {
      "type": "integer",
      "description": "1-based, increasing."
     },
     "definition": {
      "$ref": "#/components/schemas/WfFlowDefinition",
      "description": "OMITTED from the versions LIST; present on the single-version read."
     },
     "publishedBy": {
      "type": "string",
      "format": "uuid",
      "description": "Who published it (a user id). Absent — never null — when a machine principal or the dev principal published it: a version names a person or nobody."
     },
     "changeMessage": {
      "type": "string",
      "description": "Kept for old clients: nothing writes it, so it is empty. `change.summary` (on the list) says what changed."
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "onCanvas": {
      "type": "boolean",
      "description": "Marks the AT MOST ONE version whose snapshot equals the current DRAFT. Computed only by the LIST (`false` on a single read). Distinct from LIVE, which is the workflow's `latestVersionId`."
     },
     "change": {
      "type": "object",
      "description": "LIST only: what this version changed against the one before it, keyed on step name, as the list shows it. Absent on the single-version read, and on the last row of a page when the version before it is not in hand.",
      "properties": {
       "first": {
        "type": "boolean",
        "description": "The first version: nothing to compare with."
       },
       "added": {
        "type": "array",
        "items": {
         "type": "string"
        },
        "description": "Steps added, by display name, in canvas order."
       },
       "removed": {
        "type": "array",
        "items": {
         "type": "string"
        },
        "description": "Steps removed, by display name."
       },
       "changed": {
        "type": "array",
        "items": {
         "type": "string"
        },
        "description": "Steps changed, by display name."
       },
       "trigger": {
        "type": "string",
        "description": "How the way it starts changed, in words (\"Webhook replaced Schedule\", \"Webhook reconfigured\"); absent when it did not."
       },
       "settings": {
        "type": "boolean",
        "description": "The workflow-level settings changed (timeout, concurrency, error workflow)."
       },
       "summary": {
        "type": "string",
        "description": "The one-line form the row shows."
       }
      }
     }
    },
    "required": [
     "id",
     "workflowId",
     "versionNumber",
     "createdAt"
    ]
   },
   "WfWebhookRotateResult": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid",
      "description": "The workflow id."
     },
     "webhookToken": {
      "type": "string",
      "description": "The NEW token. The public URL is the API's public base + `/api/webhooks/` + this value, and that endpoint is unauthenticated. The previous token is already dead."
     }
    },
    "required": [
     "id"
    ]
   },
   "WfTriggerPayload": {
    "type": "object",
    "description": "The trigger payload for a run. Any JSON object; it is reshaped to the trigger's delivery shape unless `__shaped` is true. There is NO `environment` field here — that is a query parameter precisely so it cannot collide with your payload.",
    "additionalProperties": true,
    "properties": {
     "__shaped": {
      "type": "boolean",
      "description": "Set true when the object is ALREADY in delivery shape (a replayed run's `triggerData`, or a saved test payload): the key is stripped and the rest passes through verbatim, so `method`/`headers`/`query` survive instead of being re-wrapped under `body`."
     }
    }
   },
   "WfTestStepRequest": {
    "type": "object",
    "properties": {
     "nodeId": {
      "type": "string",
      "description": "The step's stable NAME in the definition tree (`FlowAction.name`) — not a display name, not an index. Required and non-empty.",
      "minLength": 1
     },
     "prevOutputs": {
      "type": "object",
      "additionalProperties": true,
      "description": "Fake upstream results, keyed by step name, seeding `{{steps.<name>.…}}` for this test only."
     },
     "triggerData": {
      "type": "object",
      "additionalProperties": true,
      "description": "Fake trigger payload seeding `{{trigger.…}}` for this test only. Give it in delivery shape (e.g. `{\"body\": {...}}` for a webhook workflow)."
     }
    },
    "required": [
     "nodeId"
    ]
   },
   "WfTestStepResult": {
    "type": "object",
    "description": "Always delivered with HTTP 200. Branch on `success` — a failed step, a rejected config and \"no worker available\" all arrive here, not as an error status.",
    "properties": {
     "success": {
      "type": "boolean"
     },
     "output": {
      "description": "Present when `success` is true: exactly what the step produced."
     },
     "error": {
      "type": "string",
      "description": "Present when `success` is false: the step's failure message, or an explanation that no worker was available to run it (in which case the step never executed and the workflow itself is unaffected)."
     },
     "logs": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Present when `success` is false and the step wrote console lines before it failed (a Code 2.0.0 step keeps its `console.log`/`info`/`warn`/`error` lines on failure, as a run's record does). Masked like any output."
     },
     "input": {
      "description": "What the step ran with: `{\"config\": {…}}`, the step's config with expressions resolved and the app's defaults filled in, secret fields redacted and masked, as a run's record keeps it (a Code 2.0.0 step's `code` is verbatim, since nothing in it is replaced). Absent when the step never ran."
     },
     "durationMs": {
      "type": "integer",
      "description": "The step's own time in milliseconds, without the round trip to the worker. Absent when the step never ran."
     }
    },
    "required": [
     "success"
    ]
   },
   "WfVariableDTO": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string"
     },
     "value": {
      "type": "string",
      "description": "The value for a PLAIN variable. Always an empty string for a sensitive one — there is no read path for a secret."
     },
     "isSecret": {
      "type": "boolean",
      "description": "True = stored encrypted outside the definition and takes effect immediately, without a publish. False = lives in the draft definition and reaches production only when you publish."
     }
    },
    "required": [
     "name",
     "value",
     "isSecret"
    ]
   },
   "WfVariablesResponse": {
    "type": "object",
    "properties": {
     "variables": {
      "type": "array",
      "description": "Sensitive variables first, then by name.",
      "items": {
       "$ref": "#/components/schemas/WfVariableDTO"
      }
     },
     "secretsAvailable": {
      "type": "boolean",
      "description": "False when the server has no encryption key configured — writing any `isSecret` variable will then be refused with 422."
     },
     "definitionRev": {
      "type": "integer",
      "format": "int64",
      "description": "The workflow's revision as this answer leaves it. Plain variables live in the draft definition, so a `PUT` moves it: send this as `expectedRev` on your next write to the workflow (`PUT /api/workflows/{id}` or this endpoint)."
     },
     "hasUnpublishedChanges": {
      "type": "boolean",
      "description": "Whether the draft is now ahead of the published version — a variable change on a live workflow takes effect only when published."
     }
    },
    "required": [
     "variables",
     "secretsAvailable"
    ]
   },
   "WfPutVariablesRequest": {
    "type": "object",
    "description": "The COMPLETE variable set. Anything omitted is deleted. `variables` is required: `[]` deletes them all.",
    "properties": {
     "variables": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "name": {
         "type": "string",
         "description": "Trimmed. Letters, digits and underscores, not starting with a digit, at most 128 characters. Duplicates in one request are refused."
        },
        "value": {
         "type": "string",
         "description": "Required on a sensitive entry unless `keep` is true; a plain entry without it holds an empty string. For `isSecret`, at least 6 characters (bytes) and at most 8,192 bytes — a shorter secret is refused rather than stored unprotected, because run-data masking matches the VALUE and would blank out unrelated text."
        },
        "isSecret": {
         "type": "boolean",
         "default": false
        },
        "keep": {
         "type": "boolean",
         "default": false,
         "description": "Sensitive entries only (ignored on a plain one): retain the stored value, since the client was never given it and cannot send it back. Refused with 422 if there is nothing stored under that name."
        }
       },
       "required": [
        "name"
       ]
      }
     },
     "expectedRev": {
      "type": "integer",
      "format": "int64",
      "description": "Optimistic lock, as on `PUT /api/workflows/{id}`: the `definitionRev` you last read. If the workflow has been written since, the request is refused with 409 `CONFLICT` and nothing is changed. Omit it (or send 0) for an unconditional write."
     }
    },
    "required": [
     "variables"
    ]
   },
   "PublishReview": {
    "type": "object",
    "description": "What publishing would put live (GET /api/workflows/{id}/publish-review).",
    "properties": {
     "rev": {
      "type": "integer",
      "description": "The draft revision the review describes; a publish click names it."
     },
     "baseVersionId": {
      "type": "string",
      "format": "uuid",
      "description": "The last version a person published; absent on a first publish."
     },
     "baseVersion": {
      "type": "integer",
      "description": "That version's number."
     },
     "firstPublish": {
      "type": "boolean",
      "description": "Nothing was published by a person before."
     },
     "diff": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "kind": {
         "type": "string"
        },
        "text": {
         "type": "string"
        }
       },
       "required": [
        "kind",
        "text"
       ]
      }
     },
     "callouts": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "kind": {
         "type": "string",
         "description": "address | connection | secret"
        },
        "step": {
         "type": "string"
        },
        "text": {
         "type": "string"
        }
       },
       "required": [
        "kind",
        "step",
        "text"
       ]
      }
     },
     "outline": {
      "type": "object",
      "description": "The definition that goes live, every config emptied."
     }
    },
    "required": [
     "diff",
     "callouts"
    ]
   },
   "WfPublishRequest": {
    "type": "object",
    "properties": {
     "expectedRev": {
      "type": "integer",
      "description": "The `definitionRev` of the draft the caller holds. Makes the publish conditional: a draft that moved since gives 409 and nothing is published. Omit — or send 0 or less, which is ignored — for an unconditional publish."
     },
     "review": {
      "type": "object",
      "description": "Required on a person's click on the assistant's publish card (`X-Assistant-Chat` with `X-Assistant-Click`), ignored otherwise: the comparison the card showed. 409 `KEEL_PUBLISH_STALE` when it no longer describes the workflow.",
      "properties": {
       "rev": {
        "type": "integer",
        "description": "The `definitionRev` of the draft the card showed."
       },
       "baseVersionId": {
        "type": "string",
        "description": "The last version a person published, which the card compared the draft with; empty for a first publish."
       }
      }
     }
    }
   },
   "WfRowWriteRequest": {
    "type": "object",
    "properties": {
     "expectedRev": {
      "type": "integer",
      "description": "The `definitionRev` of the draft the caller holds. Makes the write conditional: a draft that moved since gives 409 and nothing is written. Omit — or send 0 or less, which is ignored — for an unconditional write."
     }
    }
   },
   "WfRestoreVersionRequest": {
    "type": "object",
    "properties": {
     "expectedRev": {
      "type": "integer",
      "description": "The workflow's current `definitionRev`. Makes the restore conditional: a draft that moved underneath you gives 409 and writes nothing. Omit — or send 0 or less, which is ignored — for an unconditional restore."
     }
    }
   },
   "WfListenArmed": {
    "type": "object",
    "properties": {
     "testUrl": {
      "type": "string",
      "description": "The workflow's own webhook PATH (`/api/webhooks/{webhookToken}`) — there is no separate test address. Prefix with the API's public base to get a URL. Empty string for a workflow that has no token."
     },
     "expiresInSeconds": {
      "type": "integer",
      "description": "How long the one-shot listener stays armed (120)."
     }
    },
    "required": [
     "testUrl",
     "expiresInSeconds"
    ]
   },
   "WfTestContextSnapshot": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid",
      "description": "Run id — pass it back as `?snapshot=` to expand it."
     },
     "status": {
      "type": "string",
      "enum": [
       "pending",
       "running",
       "completed",
       "failed",
       "cancelled",
       "paused"
      ]
     },
     "startedAt": {
      "type": "string",
      "format": "date-time"
     },
     "source": {
      "type": "string",
      "const": "run"
     }
    },
    "required": [
     "id",
     "status",
     "startedAt",
     "source"
    ]
   },
   "WfTestContext": {
    "type": "object",
    "properties": {
     "snapshots": {
      "type": "array",
      "description": "Up to 10 recent MANUAL (test) runs, newest first. Production runs are deliberately excluded.",
      "items": {
       "$ref": "#/components/schemas/WfTestContextSnapshot"
      }
     },
     "active": {
      "type": [
       "object",
       "null"
      ],
      "description": "One expanded run, or null when the workflow has no manual runs.",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "nodes": {
        "type": "object",
        "description": "Keyed exactly the way expressions resolve: `\"trigger\"` holds the run's trigger data, every other key is a step name holding that step's output. Only steps that completed with output appear. Secret-looking keys are redacted.",
        "additionalProperties": true
       },
       "deferred": {
        "type": "object",
        "description": "Only with `large=defer`: the outputs (and `\"trigger\"`) left out of `nodes` because each is over 64 KB of JSON, keyed as `nodes` is, each with its size. Absent otherwise.",
        "additionalProperties": {
         "$ref": "#/components/schemas/DeferredValue"
        }
       }
      },
      "required": [
       "id",
       "nodes"
      ]
     }
    },
    "required": [
     "snapshots",
     "active"
    ]
   },
   "WfStepDataSource": {
    "type": "object",
    "properties": {
     "runId": {
      "type": "string",
      "format": "uuid"
     },
     "status": {
      "type": "string",
      "enum": [
       "pending",
       "running",
       "completed",
       "failed",
       "cancelled",
       "paused"
      ]
     },
     "startedAt": {
      "type": "string",
      "format": "date-time"
     },
     "environment": {
      "type": "string",
      "enum": [
       "test",
       "production"
      ],
      "description": "Both are listed — label them rather than blurring the two."
     },
     "triggerType": {
      "type": "string"
     }
    },
    "required": [
     "runId",
     "status",
     "startedAt",
     "environment",
     "triggerType"
    ]
   },
   "WfStepDataSourceList": {
    "type": "object",
    "properties": {
     "sources": {
      "type": "array",
      "description": "Up to 10 runs that provably produced data for the requested step, newest first. Empty when none did.",
      "items": {
       "$ref": "#/components/schemas/WfStepDataSource"
      }
     }
    },
    "required": [
     "sources"
    ]
   },
   "WfStepDataFromRun": {
    "type": "object",
    "properties": {
     "data": {
      "description": "The step's recorded output from that run — or the run's trigger payload when the requested step addresses the trigger. Never empty: no data is a 404, so a caller can tell \"nothing recorded\" apart from \"the step returned {}\". Secret-looking keys are redacted."
     }
    },
    "required": [
     "data"
    ]
   },
   "WfPresenceBeatRequest": {
    "type": "object",
    "properties": {
     "editing": {
      "type": "boolean",
      "default": false,
      "description": "True = an editor surface; acquire the advisory lock when free, refresh it when already held by you. False = a passive viewer, presence only."
     },
     "takeover": {
      "type": "string",
      "enum": [
       "",
       "request",
       "grant",
       "force"
      ],
      "default": "",
      "description": "`request` = ask the holder to hand over (claimable after a ~30s grace either way, so an away holder cannot strand the flow). `grant` = the HOLDER hands over now; there is no \"decline\". `force` = admin override, checked server-side against `workspace:manage` — except against your OWN lock in another tab (`mineElsewhere`), which you may always take."
     },
     "tab": {
      "type": "string",
      "maxLength": 64,
      "description": "An opaque id for the browser tab sending the beat. With it the lock is held by you IN THAT TAB: your other tabs read `mine: false, mineElsewhere: true`. Omitted = the lock is yours in every tab (older clients)."
     }
    }
   },
   "WfPresenceEditor": {
    "type": "object",
    "properties": {
     "userId": {
      "type": "string"
     },
     "name": {
      "type": "string",
      "description": "The person's name, else their email (an embedded person: the name the partner gave them), or \"someone\" when unknown."
     },
     "since": {
      "type": "string",
      "format": "date-time"
     },
     "keel": {
      "type": "boolean",
      "description": "True when this is an automated actor applying changes on a user's behalf."
     }
    },
    "required": [
     "userId",
     "name",
     "since"
    ]
   },
   "WfPresenceTakeover": {
    "type": "object",
    "description": "A pending take-over request against the current lock holder.",
    "properties": {
     "userId": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "since": {
      "type": "string",
      "format": "date-time"
     }
    },
    "required": [
     "userId",
     "name",
     "since"
    ]
   },
   "WfPresenceState": {
    "type": "object",
    "properties": {
     "editors": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/WfPresenceEditor"
      },
      "description": "Everyone currently present. Entries evaporate ~30s after the last beat."
     },
     "lock": {
      "$ref": "#/components/schemas/WfPresenceEditor",
      "description": "Who holds the advisory editing lock. Absent when it is free."
     },
     "mine": {
      "type": "boolean",
      "description": "True when the CALLER holds the lock — in this tab, when the beat names a `tab`. Computed server-side — never compare user ids yourself."
     },
     "mineElsewhere": {
      "type": "boolean",
      "description": "True when the caller holds the lock in ANOTHER tab. This tab is read-only; it may take the lock at once with `takeover: \"force\"`."
     },
     "takeover": {
      "$ref": "#/components/schemas/WfPresenceTakeover"
     },
     "beatEverySeconds": {
      "type": "integer",
      "description": "How often to beat. The server owns this interval; do not hard-code one."
     },
     "definitionRev": {
      "type": "integer",
      "format": "int64",
      "description": "The workflow's current `definitionRev`. A read-only client refetches the draft when it moves."
     }
    },
    "required": [
     "editors",
     "mine",
     "mineElsewhere",
     "beatEverySeconds",
     "definitionRev"
    ]
   },
   "Workflow": {
    "type": "object",
    "title": "Workflow",
    "description": "A workflow row. `definition` is always the DRAFT — the tree the editor holds. What production actually runs is the snapshot named by `latestVersionId`, which only changes when you publish.",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid",
      "readOnly": true
     },
     "name": {
      "type": "string",
      "maxLength": 255
     },
     "description": {
      "type": "string"
     },
     "definition": {
      "$ref": "#/components/schemas/FlowVersion"
     },
     "status": {
      "type": "string",
      "enum": [
       "draft",
       "active",
       "paused"
      ],
      "readOnly": true,
      "description": "`draft` = never published. `active` = live, triggers registered and the webhook URL reachable. `paused` = was live, now switched off. Server-owned: it moves only through publish/unpublish, never through a create or update body."
     },
     "triggerType": {
      "type": "string",
      "enum": [
       "manual",
       "cron",
       "webhook",
       "callable",
       "event",
       "error",
       "run_finished",
       "poll"
      ],
      "description": "Mirrors `definition.trigger.type` for dispatch. Derived server-side from the definition by every update and publish, so treat it as read-only in practice. A create that sends its own value keeps it until then, unchecked."
     },
     "cronExpression": {
      "type": [
       "string",
       "null"
      ],
      "description": "Nothing derives or reads this: the schedule runs from the trigger's own `config`, and `nextRunAt` projects from it too. Null unless a caller wrote one with `PUT /api/workflows/{id}`."
     },
     "webhookToken": {
      "type": [
       "string",
       "null"
      ],
      "readOnly": true,
      "description": "The secret segment of the public webhook URL. Made by the first update of a webhook workflow, or at publish; absent until then (so absent in the answers of create, duplicate and import). Returned ONLY to a caller holding `workflow:run`: by `GET /api/workflows/{id}` and in the answers of update, publish, unpublish and restore (rotate answers it to its caller). It is never present on a list row, an export or a version snapshot. It is a bearer capability against an unauthenticated endpoint — possessing it is the ability to fire this workflow in production. Rotate it with `POST /api/workflows/{id}/webhook/rotate`."
     },
     "latestVersionId": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "readOnly": true,
      "description": "The published snapshot production executes. Absent means nothing has ever been published."
     },
     "definitionRev": {
      "type": "integer",
      "format": "int64",
      "readOnly": true,
      "description": "Optimistic-lock revision, bumped by every definition save. Populated by `GET /api/workflows/{id}`, by the update response and by the answers of create, duplicate and import; absent (0) on list rows. Send it back as `expectedRev` on an update to refuse a clobbering write."
     },
     "hasUnpublishedChanges": {
      "type": "boolean",
      "readOnly": true,
      "description": "The draft has moved ahead of the published version. Computed on the single-workflow read and on every list row; always `false` for a workflow never published."
     },
     "folderId": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Absent/null = uncategorized. Changed only through `PUT /api/workflows/{id}/folder`."
     },
     "tags": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Changed only through `PUT /api/workflows/{id}/tags` or the bulk `addTags` operation — never through the workflow update body."
     },
     "testPayloads": {
      "type": [
       "object",
       "null"
      ],
      "additionalProperties": true,
      "description": "The saved test-fixture library, stored outside the definition so publish and version restore never reset it."
     },
     "pinnedData": {
      "type": [
       "object",
       "null"
      ],
      "additionalProperties": true,
      "description": "The pin library (`{\"pins\": {\"<stepName>\": {\"data\": …, \"pinnedAt\": …, \"configHash\": …, \"staleAt\": …, \"pieceId\": …}}}`). A pinned step emits its fixture instead of executing, and only on test runs. `configHash`, `staleAt` and `pieceId` are the server's: a step whose config changed keeps its pin marked stale, and a stale pin is not replayed (the step runs); a step whose app was replaced loses it."
     },
     "workspaceId": {
      "type": "string",
      "format": "uuid",
      "readOnly": true,
      "description": "Stamped from the credential's scope; never accepted from a request body."
     },
     "orgId": {
      "type": "string",
      "format": "uuid",
      "readOnly": true
     },
     "createdBy": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "readOnly": true
     },
     "createdByName": {
      "type": "string",
      "readOnly": true,
      "description": "List rows only: the creator's name, else their email address. Absent when the creator is unknown or not a member of the organization (staff who built it on a support visit, someone since removed), and on every row for an embedded person whose installation switched `presence` off (an embedded person is never told an address)."
     },
     "createdAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
     },
     "updatedAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true,
      "description": "When the workflow itself was last edited. Publishing, pausing, tagging and filing it are not edits and leave it alone."
     },
     "failing": {
      "type": "boolean",
      "readOnly": true,
      "description": "List rows only: the workflow is live (`status: active`) and its latest FINISHED production run (one that worked or failed) failed. A later run that is waiting, running, held or was cancelled changes nothing, and test runs never count. A paused or draft workflow is never failing."
     },
     "failingSince": {
      "type": "string",
      "format": "date-time",
      "readOnly": true,
      "description": "Only on a failing row: when the first production run failed after the last one that worked (runs cancelled, held or running in between neither break nor begin the streak). Left out for a caller without `run:read`."
     },
     "failingRun": {
      "type": "object",
      "readOnly": true,
      "description": "Only on a failing row: the run that decided it, its latest finished production run, with why it failed. It is `lastRun` unless a later run is waiting, running, held or was cancelled. Without `run:read` it carries `status` and `startedAt` alone.",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "status": {
        "type": "string",
        "enum": [
         "failed"
        ]
       },
       "startedAt": {
        "type": "string",
        "format": "date-time"
       },
       "finishedAt": {
        "type": [
         "string",
         "null"
        ],
        "format": "date-time"
       },
       "error": {
        "type": "object",
        "description": "Why it failed, as `lastRun.error` says it.",
        "properties": {
         "step": {
          "type": [
           "string",
           "null"
          ]
         },
         "stepKey": {
          "type": [
           "string",
           "null"
          ]
         },
         "message": {
          "type": "string"
         }
        }
       }
      }
     },
     "summary": {
      "type": "object",
      "readOnly": true,
      "description": "List rows only: what a list draws instead of the definition.",
      "required": [
       "trigger",
       "pieces",
       "stepCount",
       "needsSetup"
      ],
      "properties": {
       "trigger": {
        "type": [
         "object",
         "null"
        ],
        "description": "The chosen trigger; null while none is chosen.",
        "properties": {
         "pieceId": {
          "type": "string"
         },
         "actionId": {
          "type": "string"
         },
         "displayName": {
          "type": "string"
         }
        }
       },
       "pieces": {
        "type": "array",
        "description": "The apps the workflow uses, each once, in walk order (the trigger first), with their display names.",
        "items": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string"
          },
          "name": {
           "type": "string"
          }
         }
        }
       },
       "stepCount": {
        "type": "integer",
        "description": "Steps after the trigger, across branch arms, loop bodies and error paths."
       },
       "needsSetup": {
        "type": "integer",
        "description": "Steps (and a chosen trigger) the editor marks to configure: `valid: false`, or no app and action chosen."
       }
      }
     },
     "runs7d": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "List rows without `view=summary` only: counted runs per UTC day over the last seven days, oldest first (production runs that are not a sub-flow's, cancelled runs left out)."
     },
     "nextRunAt": {
      "type": "string",
      "format": "date-time",
      "description": "Next fire time of an active schedule workflow, projected from its cron in the trigger's own timezone. Populated by the list endpoint."
     },
     "lastRun": {
      "type": [
       "object",
       "null"
      ],
      "readOnly": true,
      "description": "The most recent PRODUCTION run's outcome (test runs never count). Populated on list rows only. Without `run:read` it carries `status` and `startedAt` alone.",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "finishedAt": {
        "type": [
         "string",
         "null"
        ],
        "format": "date-time"
       },
       "error": {
        "type": "object",
        "description": "Only when `status` is `failed`: why.",
        "properties": {
         "step": {
          "type": [
           "string",
           "null"
          ],
          "description": "The display name of the step it failed at; null when the reason names no step."
         },
         "stepKey": {
          "type": [
           "string",
           "null"
          ]
         },
         "message": {
          "type": "string",
          "description": "The reason's first clause, at most 160 characters."
         }
        }
       },
       "status": {
        "type": "string",
        "enum": [
         "pending",
         "running",
         "completed",
         "failed",
         "cancelled",
         "paused"
        ]
       },
       "startedAt": {
        "type": "string",
        "format": "date-time"
       }
      }
     }
    }
   },
   "FlowVersion": {
    "type": "object",
    "title": "FlowVersion",
    "description": "The workflow definition, and the single most important shape in this API.\n\nIt is a **TREE, not a list of steps**. There is no `steps`/`actions`/`nodes` array anywhere. The `trigger` is the root; the first step is `trigger.nextAction`; every step links to the one after it through its own `nextAction`. Steps also live inside `branches[].firstAction` (one per conditional arm), `firstLoopAction` (a loop body) and `onErrorAction` (a step's failure path). A step parked anywhere else is not part of the flow and will never execute.\n\n**Unknown keys are refused.** Any key not listed here — at this level, on the trigger, on a step, or on a branch — is a 400 whose message names the key and the legal set (a condition's own unknown keys are not: `left` for `field` is dropped). That refusal exists because JSON decoding is otherwise lossy: steps in an `actions` array, or a step whose configuration was sent as `settings` instead of `config`, used to be stored, published and run GREEN while doing nothing (or running the app's defaults).\n\nTwo vocabularies live one field apart: a **trigger's `type` is lowercase** (`webhook`, `cron`, `manual`, …) while an **action's `type` is UPPERCASE** (`PIECE`, `CODE`, `BRANCH`, `LOOP`). And the trigger names its trigger in a field called `actionId`, not `triggerId`.\n\nStep outputs are addressed by step NAME in expressions: `{{steps.<name>.<field>}}`, `{{trigger.body}}`, `{{$vars.NAME}}`. Step names must be unique across the whole tree.\n\n**Secrets read back hidden.** A field an app declares secret is answered as `[hidden secret: kept as set]` on every read, to every caller, in the workflow, its versions and its export. Send that text back in the same field of the same step to keep the value; anywhere else it is saved empty.",
    "required": [
     "schemaVersion",
     "trigger"
    ],
    "additionalProperties": false,
    "properties": {
     "schemaVersion": {
      "type": "integer",
      "enum": [
       2
      ],
      "description": "2. There is no other tree. A definition written without it reads as 2 and is stored with 2. Import refuses a file without it or with anything else; create, update and publish store and run another number as written, so write 2."
     },
     "trigger": {
      "$ref": "#/components/schemas/FlowTrigger"
     },
     "settings": {
      "$ref": "#/components/schemas/WorkflowSettings"
     },
     "annotations": {
      "description": "Canvas sticky-note metadata, stored and returned verbatim. The API never interprets it; leave it out unless you are round-tripping an existing definition."
     },
     "variables": {
      "type": "object",
      "additionalProperties": true,
      "description": "Workflow-level constants readable from any step as `{{$vars.NAME}}`. Only NON-secret values live here — this object is returned by every definition-bearing read and copied into every published snapshot and export. Secret variables are stored outside the definition and managed through `/api/workflows/{id}/variables`."
     },
     "pinnedData": {
      "type": "object",
      "additionalProperties": true,
      "description": "Legacy in-definition pin storage. Pins now live in the workflow's own `pinnedData` column (set through the workflow update body), which survives publish and version restore. Do not write this key on new definitions."
     }
    },
    "example": {
     "schemaVersion": 2,
     "trigger": {
      "name": "trigger",
      "displayName": "Order received",
      "type": "webhook",
      "pieceId": "webhook",
      "pieceVersion": "1.0.0",
      "actionId": "catch_webhook",
      "config": {
       "methods": [
        "POST"
       ]
      },
      "valid": true,
      "nextAction": {
       "name": "forward",
       "displayName": "Forward to fulfilment",
       "type": "PIECE",
       "pieceId": "http",
       "pieceVersion": "1.0.0",
       "actionId": "send_request",
       "config": {
        "method": "POST",
        "url": "https://example.com/fulfilment",
        "body_type": "json",
        "body_json": "{{trigger.body}}"
       },
       "valid": true
      }
     },
     "settings": {}
    }
   },
   "FlowTrigger": {
    "type": "object",
    "title": "FlowTrigger",
    "description": "The root of the tree — how the workflow starts. Exactly one per definition, and it is NOT a FlowAction: it has no `type: PIECE`, no `branches`, no `skip`, and it cannot be reached from any other node. The chain of steps hangs off its `nextAction`.\n\nNote the two field names that catch every integrator out: `type` is the LOWERCASE trigger vocabulary (an action's is uppercase), and the specific trigger within the app is named in `actionId` — there is no `triggerId` field.",
    "required": [
     "name"
    ],
    "additionalProperties": false,
    "properties": {
     "name": {
      "type": "string",
      "description": "The trigger's stable address. Conventionally `trigger`. Must be non-empty, and must not collide with any step name — expressions and pins key on it."
     },
     "displayName": {
      "type": "string",
      "description": "Human label shown on the canvas. Purely cosmetic; renaming it never invalidates anything."
     },
     "type": {
      "type": "string",
      "enum": [
       "manual",
       "cron",
       "webhook",
       "callable",
       "event",
       "error",
       "run_finished",
       "poll"
      ],
      "description": "LOWERCASE, unlike an action's `type`. This is what decides how the workflow is dispatched, and it is mirrored onto the workflow row: `webhook` (also used by the hosted form) mints a public URL at publish, `cron` registers with the scheduler, `poll` is polled, `error`/`run_finished` subscribe to another workflow's runs, `manual` only ever runs when triggered by hand or through the API. Not checked on a write, so a draft may hold what it was sent (blank when it was left out); a read answers the type its app means when an app is chosen."
     },
     "pieceId": {
      "type": "string",
      "description": "The app providing the trigger, e.g. `webhook`, `schedule`, `form`, `error`, `imap`. Empty means no trigger has been chosen yet — legal in a draft, refused at publish."
     },
     "pieceVersion": {
      "type": "string",
      "description": "Pins WHICH version of `pieceId` runs. Leave it out on a new trigger and the server stamps the app's default version (the one the worker fleet serves now); a trigger the stored draft already had keeps what it ran (its pin, or the app's OLDEST version when it was never pinned). Once stamped it is never rewritten, so an app update cannot change a flow already in production. A pin the fleet cannot serve is REFUSED at run time, never quietly substituted."
     },
     "actionId": {
      "type": "string",
      "description": "The trigger WITHIN the app — this field is called `actionId` even on the trigger, e.g. `catch_webhook` on `webhook`, `schedule` on `schedule`. Naming one the app does not declare publishes a workflow that never fires, so publish refuses it."
     },
     "config": {
      "type": [
       "object",
       "null"
      ],
      "additionalProperties": true,
      "description": "The trigger's configuration, keyed by the app's own field names (fetch them from `GET /api/pieces/{pieceId}/triggers/{actionId}`). It is `config`, NOT `settings` or `parameters` — a trigger sent with the wrong key loses its entire configuration. Values may be expressions (`{{$vars.TOKEN}}`). Never put a `webhookToken` here: it is server-owned and stripped on every write. Null when the definition was written without one."
     },
     "connectionId": {
      "type": [
       "string",
       "null"
      ],
      "description": "Credential this trigger uses when it must register itself with an external service. Null/absent for triggers that need none."
     },
     "valid": {
      "type": "boolean",
      "description": "\"An app and a trigger have been CHOSEN for this node\" — it does NOT mean the configuration passes the app's schema. The server derives it on every write and read: true exactly when `pieceId` and `actionId` are set; a value sent is not kept. The editor shows a to-configure count from it; what a node still needs is what `POST /api/workflows/validate` reports."
     },
     "nextAction": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/FlowAction"
       },
       {
        "type": "null"
       }
      ],
      "description": "The FIRST step of the workflow. Absent = a trigger with nothing after it, which runs and does nothing."
     }
    }
   },
   "FlowAction": {
    "type": "object",
    "title": "FlowAction",
    "description": "One step in the tree, and the recursive node type: `nextAction`, `branches[].firstAction`, `firstLoopAction` and `onErrorAction` all point at another FlowAction.\n\n`type` is UPPERCASE here — the opposite convention to the trigger's lowercase `type`. `PIECE` runs an app action, `CODE` runs JavaScript (the `code` app). A Code step's output is `{result, logs}`: what the code returns is read as `{{steps.<name>.result}}` (so return the value itself, never `{ result: … }`), and its console lines as `logs` — on Code 2.0.0 also when the step fails. On Code 2.0.0 nothing inside `config.code` is replaced, so never write `{{ }}` there: map values in `config.inputs` (`{\"email\": \"{{trigger.body.email}}\"}`) and read them as `inputs.email` (`inputs` is already declared: never `const inputs = …`), or read `steps.<name>.<field>` and `trigger.<field>` directly. A step pinned to `1.0.0` pastes each `{{ }}` into its program before it runs; `POST /api/code-steps/upgrade` converts one, `BRANCH` evaluates `branches` and runs the first arm whose conditions pass, `LOOP` iterates and runs `firstLoopAction` per item.\n\nSequence is a singly-linked list, not an array: to insert a step after `A` you set `A.nextAction` to the new step and give the new step the old `A.nextAction` as ITS `nextAction`. Names must be unique across the entire tree, including inside branch arms and loop bodies.",
    "required": [
     "name",
     "type"
    ],
    "additionalProperties": false,
    "properties": {
     "name": {
      "type": "string",
      "maxLength": 255,
      "description": "Stable identifier and the address expressions use: `{{steps.<name>.field}}`. Required, non-empty, and unique across the whole tree — a duplicate is a 400, and so is a name over 255 characters (counted as characters, not bytes) unless the workflow being replaced already holds it. Renaming a step breaks every expression pointing at the old name; nothing rewrites them for you."
     },
     "displayName": {
      "type": "string",
      "maxLength": 255,
      "description": "Human label on the canvas. Cosmetic — expressions and pins key on `name`, never on this. At most 255 characters, like `name`."
     },
     "type": {
      "type": "string",
      "enum": [
       "PIECE",
       "BRANCH",
       "LOOP",
       "CODE"
      ],
      "description": "UPPERCASE, unlike the trigger's `type`. Sending `code` for `CODE` is the single most common rejection here."
     },
     "pieceId": {
      "type": "string",
      "description": "Required for `PIECE` and `CODE` (`CODE` steps use `pieceId: \"code\"`). `BRANCH` and `LOOP` are executed by the engine itself and conventionally carry `branch`/`loop`, whose app contributes a config schema only."
     },
     "pieceVersion": {
      "type": "string",
      "description": "Pins WHICH version of `pieceId` this step runs, so a later breaking change to the app cannot alter a flow already in production. Omit it on a NEW step and a create or save stamps the app's default version (the one the fleet serves now; a Code step gets `2.0.0`; an import of a GoRunner export keeps what each step ran instead, see that operation); a step the stored draft already had keeps what it ran (its pin, or for a step never pinned the app's OLDEST version, which is what an empty pin runs). A step the draft had is recognised by its name, or, for a Code step renamed without an edit, by its code byte for byte; a Code step renamed and edited in one write is a new step. Send back the `pieceVersion` you read on every step you keep. An existing pin is never rewritten. A pin no live worker can serve makes the step FAIL, rather than silently running a different version."
     },
     "actionId": {
      "type": "string",
      "description": "The action within the app, e.g. `send_request` on `http`, `run_code` on `code`, `evaluate` on `branch`. Fetch the legal set from `GET /api/pieces/{pieceId}`."
     },
     "config": {
      "type": [
       "object",
       "null"
      ],
      "additionalProperties": true,
      "description": "The step's configuration, keyed by the action's own field names (`GET /api/pieces/{pieceId}/actions/{actionId}` returns the exact schema, including which fields are required and which are gated on a discriminator). The key is `config` — NOT `settings`, `parameters`, `inputs` or `props`; those are refused with 400 because a step whose config silently vanished would run the app's defaults on a green run. Values may be expressions: an object/array/number expression must be the ENTIRE unquoted value (`{\"payload\": {{trigger.body}}}`), only text expressions belong inside quoted strings (in a JSON field written as text, a value inside a quoted string is escaped as that string's content, so a quote or a line break in it cannot break the JSON). Per-step error handling also lives here, read off the step by the engine rather than handed to the app: `onError` (`stop` | `continue` | `errorPath`; any other value fails the run, as `stop` does), `maxRetries` (0–5; more is 5), `retryDelaySeconds` (the wait before each retry, 0–60, in place of the 2 s, 4 s, 8 s … backoff) and `timeoutSeconds` (one try's time limit, 1–600; more is 600). A number may be sent as text (`\"3\"`). A failure the app marks permanent (a 4xx, a required field left blank) is not retried, nor is a failed write that may already have been carried out (the request was sent and the reply never came): the step's error says so. Null when the step was written without one."
     },
     "connectionId": {
      "type": [
       "string",
       "null"
      ],
      "description": "Id of the stored connection supplying this step's credentials. Environment-specific: it does not survive a move to another instance, and a workspace that cannot resolve it gets the step marked invalid rather than a silent failure."
     },
     "valid": {
      "type": "boolean",
      "description": "\"An app and action have been CHOSEN\" — not a statement that the config validates. The server derives it on every write and read: true exactly when the step's app and action are chosen; a value sent is not kept. What a step still needs is what `POST /api/workflows/validate` reports."
     },
     "skip": {
      "type": "boolean",
      "description": "When true the engine skips THIS step only — its `nextAction` chain still runs."
     },
     "nextAction": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/FlowAction"
       },
       {
        "type": "null"
       }
      ],
      "description": "The next step in sequence. Absent = the chain ends here. Note it is NOT run when this step's `onErrorAction` path is taken."
     },
     "branches": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/FlowBranch"
      },
      "description": "Only for `type: BRANCH`, and a BRANCH must declare at least one arm or the definition is refused. Arms are evaluated in order and the first whose conditions pass is the one that runs. After the chosen arm finishes, execution rejoins this step's `nextAction`."
     },
     "firstLoopAction": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/FlowAction"
       },
       {
        "type": "null"
       }
      ],
      "description": "Only for `type: LOOP` — the head of the chain run once per item. Steps inside the body read the current item as `{{steps.<loopName>.item}}`. Execution continues at this step's `nextAction` when the loop finishes."
     },
     "onErrorAction": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/FlowAction"
       },
       {
        "type": "null"
       }
      ],
      "description": "The chain run when THIS step fails after its retries, and only when `config.onError` is `errorPath`. Orthogonal to `type` — any PIECE or CODE step may declare one. When taken, the failure is seeded as this step's output so the handler can read `{{steps.<name>.error}}`, and this step's NORMAL `nextAction` does NOT run: the error chain is terminal, and the run still finishes as completed."
     }
    }
   },
   "FlowBranch": {
    "type": "object",
    "title": "FlowBranch",
    "description": "One conditional arm of a `BRANCH` step. Arms are tried in array order and the FIRST one whose conditions pass is the one that runs; nothing else is evaluated. Give the last arm an empty `conditions` array to make it the catch-all.",
    "required": [
     "name",
     "conditions"
    ],
    "additionalProperties": false,
    "properties": {
     "name": {
      "type": "string",
      "description": "Label for this arm, shown on the canvas. Distinct from a step name — an arm is not a step and cannot be addressed by an expression."
     },
     "conditions": {
      "type": "array",
      "description": "A list of OR groups; each group is a list of ANDed conditions. So `[[A, B], [C]]` means `(A AND B) OR C`. An empty array passes unconditionally.",
      "items": {
       "type": "array",
       "items": {
        "$ref": "#/components/schemas/FlowCondition"
       }
      }
     },
     "firstAction": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/FlowAction"
       },
       {
        "type": "null"
       }
      ],
      "description": "Head of the chain this arm runs. Absent = an arm that matches and does nothing."
     }
    }
   },
   "FlowCondition": {
    "type": "object",
    "title": "FlowCondition",
    "description": "One comparison inside a branch arm. Note the same condition shape is also carried in a BRANCH step's `config.branches`, and THAT copy is what the engine actually evaluates — keep the two in step by sending the whole step back rather than editing one side.",
    "required": [
     "field",
     "operator"
    ],
    "additionalProperties": false,
    "properties": {
     "field": {
      "type": "string",
      "description": "The left-hand side, normally an expression: `{{steps.parse.result.total}}`, `{{trigger.body.status}}`. A literal string is compared as itself."
     },
     "operator": {
      "type": "string",
      "description": "The comparison. This is the COMPLETE set the engine implements — an operator outside it is refused where it is written (400 on create, update, import and a template's use; there is no `greaterThan`; the numeric comparison is `num_gt`), and one stored before that rule fails the step at run time. Ten of them take no right-hand `value` and ignore one if given: `text_is_empty`, `text_is_not_empty`, `bool_is_true`, `bool_is_false`, `exists`, `not_exists`, `is_empty`, `is_not_empty`, `list_is_empty`, `list_is_not_empty`.",
      "enum": [
       "text_eq",
       "text_neq",
       "text_contains",
       "text_not_contains",
       "text_starts_with",
       "text_not_starts_with",
       "text_ends_with",
       "text_not_ends_with",
       "text_is_empty",
       "text_is_not_empty",
       "num_eq",
       "num_neq",
       "num_gt",
       "num_gte",
       "num_lt",
       "num_lte",
       "bool_is_true",
       "bool_is_false",
       "exists",
       "not_exists",
       "is_empty",
       "is_not_empty",
       "regex_match",
       "regex_not_match",
       "date_eq",
       "date_neq",
       "date_after",
       "date_before",
       "date_after_or_eq",
       "date_before_or_eq",
       "list_contains",
       "list_not_contains",
       "list_is_empty",
       "list_is_not_empty",
       "list_length_eq",
       "list_length_neq",
       "list_length_gt",
       "list_length_gte",
       "list_length_lt",
       "list_length_lte"
      ]
     },
     "value": {
      "description": "The right-hand side. Any JSON type, and may itself be an expression. Omit it for the ten valueless operators listed above."
     }
    }
   },
   "WorkflowSettings": {
    "type": "object",
    "title": "WorkflowSettings",
    "description": "Workflow-wide execution settings, living at `definition.settings`. Note this is the ONE place the key `settings` is legal — on a STEP the same word is refused, because a step's configuration belongs in `config`. All fields are optional and every zero value means \"unset / server default\".",
    "additionalProperties": false,
    "properties": {
     "timeoutSeconds": {
      "type": "integer",
      "minimum": 0,
      "description": "Caps the whole run. 0 or absent = the server default. At most 3,600 (an hour): a write that sets a value outside 0–3,600 is refused with 400 (`details.field` `settings.timeoutSeconds`); a larger value stored before that check is clamped to an hour. When it expires mid-step the run fails with a timeout naming the step it was in, and a synchronous webhook caller receives a 504.",
      "maximum": 3600
     },
     "errorWorkflowId": {
      "type": "string",
      "format": "uuid",
      "description": "Run this OTHER workflow whenever a production run of this one fails, passing the failure context as its trigger data. It fires for the runs an Error Trigger hears: never for a test from the editor, and never for a run another workflow called as a Sub-flow, whose failure fails its caller (the caller's own setting then applies, once). It must exist, be in the same workspace and be published, or publishing THIS workflow is refused. If the handler already subscribes to this workflow with an Error Trigger, this setting stands down so one failure never fires two runs."
     },
     "concurrencyLimit": {
      "type": "integer",
      "minimum": 0,
      "description": "Maximum production runs of THIS workflow executing or held (paused) at once. 0 or absent = unlimited. A run over it WAITS in line and starts in order as runs finish (status `waiting`); a schedule skips its tick while one of its runs still waits. An editor's test run is never held by it. With `whenFull: refuse` the excess is refused instead."
     },
     "whenFull": {
      "type": "string",
      "enum": [
       "wait",
       "refuse"
      ],
      "description": "What a production run does when the workflow is at `concurrencyLimit`: `wait` (the default, also when absent) waits in line; `refuse` keeps the behaviour before v2026.10.4 — a webhook or API caller gets 429 `CONCURRENCY_LIMIT`, a hosted form says it is at capacity, a schedule skips its tick and a polling trigger keeps the items for its next poll."
     },
     "maintenanceMode": {
      "type": "boolean",
      "description": "Opt in to automatic self-healing: repeated identical failures open an assistant session that diagnoses and drafts a fix into the DRAFT. Nothing it produces reaches production without a publish."
     },
     "autoPublishFixes": {
      "type": "boolean",
      "description": "Recorded but NOT yet honoured — a self-healing fix currently always waits in the draft regardless of this flag."
     }
    }
   },
   "WorkflowPageInfo": {
    "type": "object",
    "title": "WorkflowPageInfo",
    "description": "Pagination block returned alongside a workflow list.",
    "properties": {
     "page": {
      "type": "integer"
     },
     "limit": {
      "type": "integer",
      "description": "The limit ACTUALLY applied after coercion, not necessarily the one you asked for."
     },
     "total": {
      "type": "integer",
      "description": "Total matching workflows across all pages."
     },
     "totalPages": {
      "type": "integer"
     }
    },
    "example": {
     "page": 1,
     "limit": 20,
     "total": 9,
     "totalPages": 1
    }
   },
   "WorkflowValidationReport": {
    "type": "object",
    "title": "WorkflowValidationReport",
    "description": "The result of judging a definition. Always delivered with HTTP 200 — read `ok`, never the status code.",
    "properties": {
     "ok": {
      "type": "boolean",
      "description": "True when nothing BLOCKING was found. Warnings and info diagnostics can be present with `ok: true` — that is the point of the distinction. A blocking diagnostic is exactly what a save or publish will refuse on."
     },
     "diagnostics": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/WorkflowDiagnostic"
      },
      "description": "Every problem found, capped at 200."
     },
     "counts": {
      "type": "object",
      "description": "Diagnostic counts keyed by severity (`error`, `warning`, `info`). Severities with no diagnostics are absent.",
      "additionalProperties": {
       "type": "integer"
      }
     },
     "skipped": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Check families that did NOT run — most often because the worker fleet is unavailable, which would otherwise make every app look unknown. A short diagnostics list plus a non-empty `skipped` is not a clean bill of health."
     },
     "truncated": {
      "type": "integer",
      "description": "How many further diagnostics were dropped by the 200 cap. Absent or 0 means the list is complete."
     }
    },
    "example": {
     "ok": false,
     "diagnostics": [
      {
       "severity": "error",
       "code": "unknown_action",
       "path": "trigger.nextAction",
       "step": "forward",
       "message": "step \"forward\" references unknown action \"sendRequest\" on app \"http\" version 1.0.0",
       "alternatives": [
        "send_request"
       ],
       "blocking": true
      }
     ],
     "counts": {
      "error": 1
     }
    }
   },
   "WorkflowDiagnostic": {
    "type": "object",
    "title": "WorkflowDiagnostic",
    "description": "One problem found in a definition. When a fix has a closed set of legal values, `alternatives` names it — that field is the fastest route from a rejection to a correct definition.",
    "properties": {
     "severity": {
      "type": "string",
      "enum": [
       "error",
       "warning",
       "info"
      ]
     },
     "code": {
      "type": "string",
      "description": "Stable machine-readable identifier for the kind of problem. Switch on this, not on `message`. The Code step's own: `code_expression_in_code` (Code 2.0.0 code holds a `{{ steps…/trigger…/$… }}`, which 2.0.0 does not replace — in live code it blocks publish, never a save; inside string or template text it is a warning, since 2.0.0 keeps that text as written), `code_empty` (2.0.0 blank code, blocks publish; 1.0.0 empty code, a warning: it fails at run time), `code_v1_expression` (Code 1.0.0 pastes `{{ }}` into the program: a warning when it reads the trigger, else info), `code_unavailable_api` (`require(`, `import`, `fetch(`, `XMLHttpRequest`, `process.env`), `code_global_not_here` (a 1.0.0 `$` name in 2.0.0 code), `code_timeout_capped` (1.0.0 stops at 30 s whatever Timeout says), `code_output_path` (`steps.<code step>.<key>` where the output is `{result, logs}`), `code_returns_result` (`return { result: … }`), `code_syntax_error` (the code does not compile as its run compiles it, so every run that reaches the step fails before a line of it runs: the message names the line of the code as written; it blocks publish, never a save, and is a warning on a step no run reaches (skipped, or in error steps its step does not route to); a 1.0.0 step whose `{{ }}` are pasted in before it runs is not checked). A step on Code 1.0.0 only ever gets warnings and info, except for code that cannot compile (`code_syntax_error`) or is over 256 KB (`code_too_large`). `step_name_too_long` (a step's `name` or `displayName` over 255 characters: a run reaching it could not save its steps. Blocking, and refused by every write; a warning for a name the definition being replaced already holds, so a workflow stored with one can still be saved and published). `missing_required_config` blocks only at an app's default version: a step pinned to (or, unpinned, running) an older version gets it as a warning."
     },
     "path": {
      "type": "string",
      "description": "Where in the definition the problem is, e.g. `trigger.nextAction.config.body_type`."
     },
     "step": {
      "type": "string",
      "description": "The step's `name`, when the problem belongs to a step."
     },
     "field": {
      "type": "string",
      "description": "The offending config field, when there is one."
     },
     "message": {
      "type": "string",
      "description": "Human-readable explanation."
     },
     "alternatives": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "The admissible values at this position, nearest miss first."
     },
     "moreAlternatives": {
      "type": "integer",
      "description": "How many legal values were NOT listed — so you can tell a complete vocabulary from a sample. 0 or absent means `alternatives` is the whole set."
     },
     "blocking": {
      "type": "boolean",
      "description": "True when a save or publish would refuse on this. Set by the validator; the informational endpoint and the write gates cannot disagree because they run the same function."
     }
    }
   },
   "WorkflowBulkResult": {
    "type": "object",
    "title": "WorkflowBulkResult",
    "description": "Per-id outcome of a bulk operation. A 200 says the request was well formed, NOT that every workflow succeeded — always inspect `failed`.",
    "properties": {
     "op": {
      "type": "string",
      "enum": [
       "activate",
       "pause",
       "delete",
       "addTags",
       "move"
      ]
     },
     "succeeded": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      },
      "description": "Always present, possibly empty."
     },
     "failed": {
      "type": "array",
      "description": "Always present, possibly empty. One entry per id that did not apply, each with its own reason.",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string",
         "description": "The id as sent — not necessarily a valid UUID."
        },
        "error": {
         "type": "string",
         "description": "Why this one failed, e.g. \"Workflow not found.\", \"too many tags (max 50)\", or a publish refusal's sentence."
        },
        "step": {
         "type": "string",
         "description": "An `activate` refused for one step: that step's display name."
        }
       }
      }
     }
    },
    "example": {
     "op": "addTags",
     "succeeded": [
      "8a3172d2-1e17-49b3-9482-f9d97cea2848"
     ],
     "failed": [
      {
       "id": "not-a-uuid",
       "error": "Invalid workflow ID"
      }
     ]
    }
   },
   "ImportReport": {
    "type": "object",
    "description": "What a translation from another product could and could not carry. Every node of the source file that asks to RUN appears in `nodes` exactly once; canvas comments (sticky notes and other notes) are counted in `warnings` instead, since they are not steps. One bound, reported when it is reached: branches and loops nested deeper than this product's own structural limit are not followed, because the alternative is letting a file decide how deep the server recurses. Real flows nest a handful of levels.",
    "properties": {
     "format": {
      "type": "string",
      "description": "Detected from the file: always `external` (another automation tool's export).",
      "enum": [
       "external"
      ]
     },
     "name": {
      "type": "string",
      "description": "The name the source file gave the workflow."
     },
     "nodes": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ImportReportNode"
      }
     },
     "mappedCount": {
      "type": "integer"
     },
     "substitutedCount": {
      "type": "integer"
     },
     "parkedCount": {
      "type": "integer"
     },
     "needsAttention": {
      "type": "integer",
      "description": "How many steps cannot run as imported — the number that decides whether this is a draft to finish or a draft to publish."
     },
     "stepsWithLeftovers": {
      "type": "integer",
      "description": "How many nodes still hold settings the translation could not place, whether or not they can run. NOT the same question as needsAttention: a step can run exactly as imported and still be carrying settings the original had, so a flow can be fully valid and still have translation left in it."
     },
     "warnings": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "File-level things the reader should know: a shape that had to be flattened (the source ran a fan-out in parallel, this product runs one chain), nodes nothing reached, comments left behind, expressions that need rewriting."
     }
    },
    "required": [
     "format",
     "name",
     "nodes",
     "mappedCount",
     "substitutedCount",
     "parkedCount",
     "needsAttention",
     "stepsWithLeftovers"
    ]
   },
   "ImportReportNode": {
    "type": "object",
    "description": "One node of the source file, and what became of it.",
    "properties": {
     "sourceName": {
      "type": "string",
      "description": "What the source file called it, so a person can find it there."
     },
     "sourceType": {
      "type": "string",
      "description": "The source node's own short type, without the source tool's package prefix, e.g. `slack` or `httpRequest`."
     },
     "outcome": {
      "type": "string",
      "enum": [
       "mapped",
       "substituted",
       "parked"
      ],
      "description": "`mapped` — an app here does that job. `substituted` — no app for it, but a generic one does the same work. `parked` — nothing here does it, so the step is an invalid No Op carrying the original settings."
     },
     "pieceId": {
      "type": "string",
      "description": "The GoRunner app it became. `no-op` on a parked step."
     },
     "stepName": {
      "type": "string",
      "description": "Its name here — what an expression referencing it says."
     },
     "ready": {
      "type": "boolean",
      "description": "Whether the step runs as imported. False asks for a visit: a connector can be MAPPED and still need its fields, because a foreign node's settings rarely mean the same thing here."
     },
     "note": {
      "type": "string",
      "description": "The honest sentence for this row."
     },
     "leftovers": {
      "type": "integer",
      "description": "How many of the source's own settings are still on this node because there was no field for them. Present even when `ready` is true — a step can run and still be unfinished."
     }
    },
    "required": [
     "sourceName",
     "sourceType",
     "outcome",
     "stepName",
     "ready",
     "note"
    ]
   },
   "WorkflowExportEnvelope": {
    "type": "object",
    "title": "WorkflowExportEnvelope",
    "description": "The portable file format produced by the export route and accepted by import. Carries no ids, no status, no folder, no webhook token and no run history — only what is meaningful on another instance.",
    "required": [
     "gorunnerExport",
     "version",
     "workflow"
    ],
    "properties": {
     "gorunnerExport": {
      "type": "string",
      "enum": [
       "workflow"
      ],
      "description": "Format marker; the value is the literal string `workflow`, not a boolean."
     },
     "version": {
      "type": "integer",
      "enum": [
       1
      ],
      "description": "Export FORMAT version — 1. Unrelated to `definition.schemaVersion`, which is 2."
     },
     "exportedAt": {
      "type": "string",
      "format": "date-time",
      "description": "When the export was produced. Informational; import ignores it."
     },
     "workflow": {
      "$ref": "#/components/schemas/WorkflowExportBody"
     }
    }
   },
   "WorkflowExportBody": {
    "type": "object",
    "title": "WorkflowExportBody",
    "description": "The portable payload inside an export envelope. Import also accepts this object on its own, without the envelope around it.",
    "required": [
     "definition"
    ],
    "properties": {
     "name": {
      "type": "string",
      "maxLength": 255,
      "description": "On import, an empty or absent name becomes \"Imported Workflow\"."
     },
     "description": {
      "type": "string"
     },
     "triggerType": {
      "type": "string",
      "enum": [
       "manual",
       "cron",
       "webhook",
       "callable",
       "event",
       "error",
       "run_finished",
       "poll"
      ],
      "description": "Overridden by whatever `definition.trigger` says, so it is effectively informational."
     },
     "tags": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Trimmed and deduplicated case-insensitively on import."
     },
     "definition": {
      "$ref": "#/components/schemas/FlowVersion"
     }
    }
   },
   "ConnectionResource": {
    "type": "object",
    "description": "A stored credential set for one app. The encrypted credential itself is never present in this object — the model tags it `json:\"-\"` — so there is no field here, and no endpoint anywhere, that returns a secret you wrote.",
    "required": [
     "id",
     "pieceId",
     "name",
     "authType",
     "status",
     "createdAt",
     "updatedAt"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid",
      "description": "The connection's id. This is the value a workflow step or trigger carries as `connectionId` to use these credentials."
     },
     "pieceId": {
      "type": "string",
      "description": "The app these credentials belong to (e.g. `seatable`, `postgres`, `googlesheets`, or the generic `oauth2`). Fixed at creation and not changeable.",
      "example": "seatable"
     },
     "name": {
      "type": "string",
      "description": "Human label. Not unique."
     },
     "authType": {
      "type": "string",
      "description": "Free-text label recording the auth shape. Defaults to `api_key` when not supplied at creation. Conventionally one of the app's `auth.type` values, but NOT validated against the app — treat it as advisory.",
      "example": "api_key"
     },
     "status": {
      "type": "string",
      "enum": [
       "active",
       "pending",
       "unverified",
       "error",
       "expired"
      ],
      "description": "The last known verdict on these credentials. `active` = created normally, or the last test passed. `pending` = an OAuth2 connection that has not completed the authorization flow (no access token yet). `unverified` = the last test proved nothing because the app has no connection test; credentials are checked on first real use. `error` = the last test was refused by the provider. `expired` = a refresh was refused for good — the provider rejected the stored grant or the client (revoked, aged out), or the OAuth app that issued the tokens was replaced or removed; signing in again (Re-authorize) mends it. The test endpoint (and `test: true` on create or update), the OAuth2 sign-in and a refused refresh set it; an update that sends `config` resets it to `active` (`pending` for a sign-in with no token); a rename leaves it."
     },
     "lastTestedAt": {
      "type": "string",
      "format": "date-time",
      "description": "When the connection was last tested. Absent if never tested."
     },
     "tokenExpiresAt": {
      "type": "string",
      "format": "date-time",
      "description": "OAuth2 access-token expiry, when known. Refresh happens automatically at run time; there is no manual refresh call."
     },
     "refreshable": {
      "type": "boolean",
      "description": "The stored config carries a refresh token, so `tokenExpiresAt` is when the access token is next RENEWED (on use), not when the connection ends. False for pasted access tokens and API keys. A connection whose refresh the provider refuses for good is marked `status: expired` on the next refresh attempt and needs signing in again."
     },
     "managedBy": {
      "type": "string",
      "enum": [
       "partner"
      ],
      "description": "Present only on a connection the installation's partner manages through the embed API. It is read-only here: update, delete and the OAuth2 sign-in answer 403 `MANAGED_CONNECTION`; test and replace stay open."
     },
     "workspaceId": {
      "type": "string",
      "format": "uuid",
      "description": "Owning workspace. Populated on the create's answer; the all-zero UUID on the list, the single read and an update's answer, which read the row the way the list does and do not select it."
     },
     "orgId": {
      "type": "string",
      "format": "uuid",
      "description": "Owning organization. Same caveat as `workspaceId`: all-zero except in the create's answer."
     },
     "createdBy": {
      "type": "string",
      "format": "uuid",
      "description": "The user who created it. Absent when no person did (the partner's, a machine's, rows from before creators were recorded)."
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "updatedAt": {
      "type": "string",
      "format": "date-time"
     },
     "account": {
      "type": "string",
      "description": "Who or what the credential is, never a secret: the Google address a test names, `user@host` (with port and database) from a typed credential's non-secret fields, a SeaTable base's name and server. Shown to every reader. Absent until known.",
      "example": "warehouse@fieldstone.example · smtp.office365.com:587"
     },
     "createdByName": {
      "type": "string",
      "description": "The creator's name (else their address), only while they are a member of the organization."
     },
     "createdByMember": {
      "type": "boolean",
      "description": "False once the creator is no longer a member of the organization. Absent with `createdBy`."
     },
     "lastError": {
      "$ref": "#/components/schemas/ConnectionLastError"
     },
     "lastUse": {
      "$ref": "#/components/schemas/ConnectionLastUse"
     },
     "oauthClient": {
      "type": "string",
      "enum": [
       "managed",
       "byo",
       "pasted"
      ],
      "description": "For a connection that signs in: `managed` — the installation's app; `byo` — its own client id; `pasted` — an access token with no refresh token, which cannot renew itself."
     },
     "lastSignInAt": {
      "type": "string",
      "format": "date-time",
      "description": "When a sign-in last stored tokens."
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time",
      "description": "When the connection stops working with no way to renew: `tokenExpiresAt` of a connection that is not `refreshable`. Absent for a refreshable one and when unknown."
     },
     "test": {
      "type": "object",
      "description": "Only on the answer of a create or a change sent with `test: true`: the test that let it be saved.",
      "required": [
       "success"
      ],
      "properties": {
       "success": {
        "type": "boolean",
        "description": "Always `true` here: a refused test saves nothing and is a 422."
       },
       "message": {
        "type": "string",
        "description": "The app's own words."
       },
       "unverified": {
        "type": "boolean",
        "description": "Present and true when the app cannot be tested without running a real step: saved, and nothing was proven."
       }
      }
     }
    }
   },
   "ConnectionLastError": {
    "type": "object",
    "description": "The last failure that still stands. Written by a failed test, a refused refresh (`expired`), and a run's or poll's definite refusal; a run's 403 is kept as news (`cause: forbidden`) without changing `status`. Cleared by a working test, a sign-in, a config change, and the next run or poll that uses the connection and works.",
    "required": [
     "at",
     "source",
     "cause",
     "message"
    ],
    "properties": {
     "at": {
      "type": "string",
      "format": "date-time"
     },
     "source": {
      "type": "string",
      "enum": [
       "test",
       "run",
       "trigger",
       "refresh",
       "signin"
      ]
     },
     "cause": {
      "type": "string",
      "enum": [
       "refused",
       "expired",
       "server",
       "forbidden",
       "other"
      ],
      "description": "`refused` — the service refused the credential (401, SMTP 535, Postgres 28P01, MySQL 1045, …); `expired` — a refresh was refused for good; `server` — the server could not be reached; `forbidden` — a 403, news only; `other`."
     },
     "message": {
      "type": "string",
      "description": "The service's own words, masked of the step's secrets, at most 1,000 characters."
     },
     "runId": {
      "type": "string",
      "format": "uuid"
     },
     "workflowId": {
      "type": "string",
      "format": "uuid"
     },
     "workflowName": {
      "type": "string",
      "description": "While the workflow exists."
     }
    }
   },
   "ConnectionLastUse": {
    "type": "object",
    "description": "The newest use the audit trail records (its `connection.decrypt` rows). Absent when the trail has none.",
    "required": [
     "at",
     "kind"
    ],
    "properties": {
     "at": {
      "type": "string",
      "format": "date-time"
     },
     "kind": {
      "type": "string",
      "enum": [
       "run",
       "trigger",
       "test",
       "editor",
       "signin"
      ]
     },
     "runId": {
      "type": "string",
      "format": "uuid"
     },
     "workflowId": {
      "type": "string",
      "format": "uuid"
     },
     "workflowName": {
      "type": "string"
     },
     "byName": {
      "type": "string",
      "description": "The person, for a test, the builder or a sign-in, while a member of the organization."
     }
    }
   },
   "ConnectionDraftTestRequest": {
    "type": "object",
    "properties": {
     "pieceId": {
      "type": "string",
      "description": "The app, for an add. Required without `connectionId`."
     },
     "connectionId": {
      "type": "string",
      "format": "uuid",
      "description": "For a change: the draft is laid over this connection's stored config (a blank field keeps the saved value). Same app, same workspace."
     },
     "config": {
      "type": "object",
      "additionalProperties": true
     }
    }
   },
   "ConnectionDraftTestResult": {
    "type": "object",
    "required": [
     "success"
    ],
    "properties": {
     "success": {
      "type": "boolean"
     },
     "message": {
      "type": "string"
     },
     "account": {
      "type": "string"
     },
     "error": {
      "type": "string"
     },
     "cause": {
      "type": "string",
      "enum": [
       "refused",
       "expired",
       "server",
       "forbidden",
       "other"
      ]
     },
     "noVerdict": {
      "type": "boolean",
      "description": "No verdict came back (no worker, the time ran out, an app this build does not serve): the only case a form offers to save without a test."
     },
     "reason": {
      "type": "string",
      "enum": [
       "no_worker",
       "timeout",
       "not_served"
      ]
     }
    }
   },
   "ConnectionOAuth2DraftStartRequest": {
    "type": "object",
    "required": [
     "pieceId"
    ],
    "properties": {
     "pieceId": {
      "type": "string",
      "description": "An app that signs in (declares OAuth2, or the generic `oauth2`). A trigger's app is made under its action app."
     },
     "name": {
      "type": "string",
      "maxLength": 255,
      "description": "Optional. Blank: \"{Service} · {account}\" once the account is known."
     },
     "config": {
      "type": "object",
      "additionalProperties": true,
      "description": "Only an own app's `clientId`, `clientSecret`, `scope` (and the generic app's `authUrl`, `tokenUrl`). Sealed into the attempt."
     },
     "mode": {
      "type": "string",
      "enum": [
       "popup",
       "tab"
      ],
      "default": "popup"
     },
     "returnTo": {
      "type": "string",
      "maxLength": 512,
      "description": "Required with `mode: tab`: the dashboard path to come back to (one leading `/`, no host)."
     }
    }
   },
   "ConnectionCreateRequest": {
    "type": "object",
    "required": [
     "pieceId",
     "name"
    ],
    "properties": {
     "pieceId": {
      "type": "string",
      "description": "The app this credential is for. Must be an app the live worker fleet runs, and that app must declare `auth.required: true` — see `GET /api/pieces/{id}`.",
      "example": "seatable"
     },
     "name": {
      "type": "string",
      "maxLength": 255,
      "description": "Human label. Trimmed; required and must not be blank; at most 255 characters (counted in characters, not bytes)."
     },
     "authType": {
      "type": "string",
      "description": "Optional free-text label for the auth shape, at most 50 characters. Defaults to `api_key`. Not validated against the app and does not change how the credential is used — the app decides that.",
      "example": "api_key",
      "maxLength": 50
     },
     "config": {
      "type": "object",
      "additionalProperties": true,
      "description": "The credential itself. Keys are the `name`s of the app's `auth.fields[]` (from `GET /api/pieces/{pieceId}`); a field of `type: \"password\"` holds a secret. For an OAuth2 app this normally holds only `clientId` and `clientSecret` — the authorize/token URLs and scopes come from the app. For the generic `oauth2` app you also supply `authUrl`, `tokenUrl` and `scope` yourself. Encrypted at rest and NEVER returned by any endpoint. Fields the app marks `required` must have a value (400 naming them). The rest are stored as sent, and keys the app does not declare are stored and ignored, so a misspelled optional key surfaces only at test or run time.",
      "example": {
       "botToken": "xoxb-..."
      }
     },
     "test": {
      "type": "boolean",
      "description": "Test the config first and save only if the service accepts it: refused → 422 `CONNECTION_TEST_FAILED` (`details.cause`), no verdict → 503 `NO_VERDICT` (`details.reason`), nothing saved in either case. Accepted → 201 with the row tested now and `test`."
     },
     "oauthApp": {
      "type": "string",
      "enum": [
       "installation"
      ],
      "description": "Only with OAuth2 tokens in `config` (an `accessToken`): they came from the installation's app for this service, which refreshes them. Refused without tokens, with `clientId` in `config`, or when the installation has no app for the service."
     }
    }
   },
   "ConnectionUpdateRequest": {
    "type": "object",
    "required": [],
    "properties": {
     "name": {
      "type": "string",
      "maxLength": 255,
      "description": "New label; trimmed; blank is a 400; at most 255 characters. Optional when `config` is sent (absent keeps the stored name); required otherwise."
     },
     "authType": {
      "type": "string",
      "description": "Optional, at most 50 characters. An empty string or omission leaves the stored value unchanged.",
      "maxLength": 50
     },
     "config": {
      "type": "object",
      "additionalProperties": true,
      "description": "OPTIONAL, and its ABSENCE (`null` reads as absent) is the signal for a rename-only update (the verdict is kept). Sent, it is MERGED over the stored config: a key left out, or a string left blank, keeps the saved value; `null` removes a key; anything else replaces it. A sign-in's tokens are kept unless named. Any config change resets the verdict: `status` `active` (or `pending` for a sign-in with no token), `lastTestedAt` and `lastError` cleared, `account` recomputed. A change that moves a where field (host, server, port, database, a URL, a connection string) must send every stored secret again, else 422 `SECRETS_REQUIRED` (`details.fields` names their keys, `details.changed` the where keys)."
     },
     "test": {
      "type": "boolean",
      "description": "Test the merged config first, as on create: refused → 422 `CONNECTION_TEST_FAILED`, no verdict → 503 `NO_VERDICT`, nothing changed."
     },
     "oauthApp": {
      "type": "string",
      "enum": [
       "installation"
      ],
      "description": "Only with OAuth2 tokens in `config` (an `accessToken`): they came from the installation's app for this service, which refreshes them. Refused without tokens, with `clientId` in `config`, or when the installation has no app for the service."
     }
    }
   },
   "ConnectionTestResult": {
    "type": "object",
    "description": "The verdict from a connection test. Delivered inside an HTTP 200 in every case, INCLUDING failure and \"no worker available\" — branch on `success`/`unverified`, never on the status code.",
    "required": [
     "success"
    ],
    "properties": {
     "success": {
      "type": "boolean",
      "description": "True when the test did not fail. Note that `success: true` together with `unverified: true` means nothing was actually proven."
     },
     "unverified": {
      "type": "boolean",
      "description": "Present and true when the app cannot verify credentials without running a real step. The connection row is set to `unverified` and the credentials are checked on first real use. Deliberately not reported as a pass."
     },
     "message": {
      "type": "string",
      "description": "The app's own words on success, or the explanation of an unverified result."
     },
     "error": {
      "type": "string",
      "description": "Present when `success` is false. Either the provider's refusal (the row becomes `error`, or `pending` for an unauthorized OAuth2 connection), or the fixed text \"No worker is available to run this right now...\" — in which case NOTHING was learned about the credentials and the stored status is deliberately left unchanged."
     },
     "cause": {
      "type": "string",
      "enum": [
       "refused",
       "expired",
       "server",
       "forbidden",
       "other"
      ],
      "description": "Why a failed test failed. `expired`: the refresh was refused during the test and the connection stays `expired`."
     },
     "noVerdict": {
      "type": "boolean",
      "description": "Present and true when no verdict came back (no worker, the time ran out, an app this build does not serve): nothing was learned about the credentials and the stored status is unchanged."
     },
     "reason": {
      "type": "string",
      "enum": [
       "no_worker",
       "timeout",
       "not_served"
      ],
      "description": "Why, with `noVerdict`."
     },
     "account": {
      "type": "string",
      "description": "Who the credential signs in as, when the test learned it."
     },
     "status": {
      "type": "string",
      "description": "The status recorded."
     },
     "unavailable": {
      "type": "boolean",
      "description": "This installation does not serve the app: nothing could test it, and nothing was recorded."
     }
    }
   },
   "ConnectionUsageMap": {
    "type": "object",
    "description": "Reverse index: connection id → the workflows referencing it. A connection with no usage is ABSENT from the map rather than mapped to an empty array; treat a missing key as zero usage.",
    "additionalProperties": {
     "type": "array",
     "items": {
      "type": "object",
      "required": [
       "id",
       "name"
      ],
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid",
        "description": "The referencing workflow's id."
       },
       "name": {
        "type": "string",
        "description": "The referencing workflow's name."
       },
       "status": {
        "type": "string",
        "enum": [
         "draft",
         "active",
         "paused"
        ],
        "description": "The referencing workflow's state. `active` is live, but only `inLive` says its production runs use the connection: a live workflow whose draft alone names it does not use it yet."
       },
       "inLive": {
        "type": "boolean",
        "description": "The workflow's published version (what production runs) names the connection. False for a workflow never published."
       },
       "inDraft": {
        "type": "boolean",
        "description": "The workflow's draft names the connection."
       }
      }
     }
    }
   },
   "ConnectionReplaceRequest": {
    "type": "object",
    "required": [
     "targetId"
    ],
    "properties": {
     "targetId": {
      "type": "string",
      "format": "uuid",
      "description": "The connection to repoint references to. Must exist in the same workspace, must differ from the path id, and must have the SAME `pieceId` as the source."
     },
     "workflowIds": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      },
      "minItems": 1,
      "maxItems": 500,
      "description": "Move only these workflows (their drafts and every version). The answer's `workflowIds` moved back the other way is an exact Undo."
     }
    }
   },
   "ConnectionReplaceResult": {
    "type": "object",
    "required": [
     "updated",
     "workflowIds",
     "alsoOnTarget"
    ],
    "properties": {
     "updated": {
      "type": "integer",
      "description": "How many workflows were moved (drafts and every stored version, counted once per workflow)."
     },
     "workflowIds": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      },
      "description": "The workflows moved."
     },
     "alsoOnTarget": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      },
      "description": "Moved workflows that already named the target before: moving them back moves those steps too."
     }
    }
   },
   "ConnectionOAuth2StartResult": {
    "type": "object",
    "required": [
     "attemptId",
     "launchUrl",
     "bindSecret",
     "apiOrigin",
     "provider",
     "redirectUri",
     "expiresAt"
    ],
    "properties": {
     "attemptId": {
      "type": "string",
      "description": "Names this sign-in in the launch handshake and in the status route."
     },
     "launchUrl": {
      "type": "string",
      "format": "uri",
      "description": "Point the popup opened on the click here. It is useless on its own: the page asks its opener for `bindSecret` and goes nowhere without it."
     },
     "bindSecret": {
      "type": "string",
      "description": "Single-use. Give it only to the popup, by postMessage to `apiOrigin`, when it asks. Never put it in a URL or storage."
     },
     "apiOrigin": {
      "type": "string",
      "description": "The exact origin the launch and result pages are served on. Accept messages only from the popup AND this origin."
     },
     "provider": {
      "type": "string",
      "description": "The provider key of the app's declared authorize URL (`google`, `github`, …); empty for the generic `oauth2` app."
     },
     "redirectUri": {
      "type": "string",
      "format": "uri",
      "description": "The callback this server is redirected back to (`{PUBLIC_API_URL}/api/oauth2/callback`). Register exactly this with the provider. It is a browser endpoint returning HTML; you never call it."
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time",
      "description": "When this attempt stops being usable (15 minutes after the press)."
     },
     "mode": {
      "type": "string",
      "enum": [
       "popup",
       "tab"
      ],
      "description": "How the window asked to sign in: `tab` is opened with `POST /api/connections/oauth2/attempts/{attemptId}/go`."
     }
    }
   },
   "ConnectionOAuth2Attempt": {
    "type": "object",
    "required": [
     "status"
    ],
    "properties": {
     "status": {
      "type": "string",
      "enum": [
       "pending",
       "launched",
       "connected",
       "failed",
       "expired"
      ],
      "description": "`pending` — the popup has not bound yet; `launched` — the person is at the provider; `connected` — the tokens are stored; `failed` — see `error`; `expired` — the attempt aged out, or never existed."
     },
     "connectionId": {
      "type": "string",
      "format": "uuid",
      "description": "The connection signed in: for a sign-in that makes its connection, present once it connected."
     },
     "reason": {
      "type": "string",
      "enum": [
       "denied",
       "other_window",
       "service_error",
       "failed",
       "cancelled"
      ],
      "description": "Only when `failed`: `denied` — the person said no on the provider's page; `other_window` — the sign-in finished in a browser that did not start it; `service_error` — the provider answered with an error; `failed` — anything on this side (the connection, its client, storage)."
     },
     "error": {
      "type": "string",
      "description": "Only when `failed`: why it ended, as a sentence for a person."
     },
     "account": {
      "type": "string",
      "description": "Only when `connected`: who signed in, when the test after the sign-in named them."
     },
     "previousAccount": {
      "type": "string",
      "description": "Only when a sign-in again signed in as someone else than before: who it was."
     },
     "accountChanged": {
      "type": "boolean"
     }
    }
   },
   "ConnectionOAuth2APIStartRequest": {
    "type": "object",
    "required": [
     "returnUrl"
    ],
    "description": "An API sign-in (an API token here; the partner's embed key on `…/accounts/{accountId}/connections/oauth2/start`): a new connection (`pieceId`, `name`, `config`) or a sign-in again (`connectionId`), and where the browser comes back to.",
    "properties": {
     "pieceId": {
      "type": "string",
      "description": "Required for a new connection: an app that signs in (declares OAuth2, or the generic `oauth2`). A trigger's app is made under its action app (`gmailtrigger` → `gmail`). With `connectionId`, optional and must be that connection's."
     },
     "name": {
      "type": "string",
      "maxLength": 255,
      "description": "A new connection's name. Blank: \"{Service} · {account}\" once the account is known. Not with `connectionId`."
     },
     "connectionId": {
      "type": "string",
      "format": "uuid",
      "description": "Sign this existing connection (in the caller's workspace) in again; it keeps its name and its app. Not a partner-managed one."
     },
     "returnUrl": {
      "type": "string",
      "format": "uri",
      "maxLength": 2048,
      "description": "Where the browser comes back to: an absolute https address (http only for localhost), no user info or `#fragment`, whose origin is exactly an allowed website (or, with an API token on `POST /api/connections/oauth2/start`, this installation's own app address). Its own query is kept; `status`, `connectionId`/`reason` and `attemptId` are set."
     },
     "mode": {
      "type": "string",
      "enum": [
       "redirect"
      ],
      "default": "redirect"
     },
     "config": {
      "type": "object",
      "additionalProperties": true,
      "description": "Your own app instead of the installation's: `clientId`, `clientSecret`, `scope` (and the generic app's `authUrl`, `tokenUrl`), and an app's `baseUrl`. Sealed into the attempt. Not with `connectionId`."
     }
    }
   },
   "ConnectionOAuth2APIStartResult": {
    "type": "object",
    "required": [
     "attemptId",
     "authorizeUrl",
     "expiresAt"
    ],
    "properties": {
     "attemptId": {
      "type": "string",
      "description": "Names this sign-in: read it with the same credential, and it comes back on `returnUrl`."
     },
     "authorizeUrl": {
      "type": "string",
      "format": "uri",
      "description": "The provider's consent page, complete: its `state` is single-use and bound to this attempt. Send the person who should sign in there, and nobody else."
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time",
      "description": "When the link stops working (10 minutes after the start)."
     }
    }
   },
   "ConnectionOAuth2APIAttempt": {
    "type": "object",
    "required": [
     "attemptId",
     "status",
     "expiresAt"
    ],
    "properties": {
     "attemptId": {
      "type": "string"
     },
     "status": {
      "type": "string",
      "enum": [
       "pending",
       "connected",
       "failed"
      ],
      "description": "`pending` — the provider has not sent the browser back yet; `connected` — the connection holds its tokens; `failed` — see `reason`."
     },
     "connectionId": {
      "type": "string",
      "format": "uuid",
      "description": "Only when `connected`: the connection made (or signed in again)."
     },
     "reason": {
      "type": "string",
      "enum": [
       "denied",
       "service_error",
       "failed",
       "cancelled",
       "expired"
      ],
      "description": "Only when `failed`: `denied` — the person said no on the provider's page; `service_error` — the provider answered with an error; `failed` — anything on this side; `cancelled` — the sign-in was stopped (`POST /api/connections/oauth2/attempts/{attemptId}/cancel`, by the token that started it; no partner endpoint stops one); `expired` — the link ran out first."
     },
     "error": {
      "type": "string",
      "description": "Only when `failed`: why, as a sentence for a person."
     },
     "account": {
      "type": "string",
      "description": "Only when `connected`: who signed in, when the test after the sign-in named them."
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "FolderResource": {
    "type": "object",
    "description": "A node in the workflow-organization tree. Folders organize workflows only — they have no relationship to connections or templates.",
    "required": [
     "id",
     "name",
     "workflowCount",
     "createdAt",
     "updatedAt"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string",
      "description": "Trimmed of surrounding whitespace. At most 255 characters. Create and rename refuse a name a sibling already has, ignoring case (409 `NAME_TAKEN`); deleting a folder moves its children up and renames a child whose name is taken there (`<name> (2)`), so siblings never share a name."
     },
     "parentId": {
      "type": "string",
      "format": "uuid",
      "description": "Parent folder. Absent/null means a root folder. The list is flat — build the tree from this field."
     },
     "workspaceId": {
      "type": "string",
      "format": "uuid",
      "description": "Owning workspace. Populated on create; the all-zero UUID in list responses (not selected by the list query)."
     },
     "orgId": {
      "type": "string",
      "format": "uuid",
      "description": "Owning organization. Same caveat as `workspaceId`."
     },
     "workflowCount": {
      "type": "integer",
      "description": "Workflows filed DIRECTLY in this folder. Does not include workflows in descendant folders."
     },
     "liveCount": {
      "type": "integer",
      "description": "live workflows filed directly in this folder. 0 on a create's answer."
     },
     "failingCount": {
      "type": "integer",
      "description": "failing workflows filed directly in this folder (live, and the latest finished production run failed). 0 on a create's answer."
     },
     "totalWorkflowCount": {
      "type": "integer",
      "description": "workflows in this folder and every folder under it, at any depth. 0 on a create's answer."
     },
     "totalLiveCount": {
      "type": "integer",
      "description": "live workflows in this folder and every folder under it. 0 on a create's answer."
     },
     "totalFailingCount": {
      "type": "integer",
      "description": "failing workflows in this folder and every folder under it. 0 on a create's answer."
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "updatedAt": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "FolderCreateRequest": {
    "type": "object",
    "required": [
     "name"
    ],
    "properties": {
     "name": {
      "type": "string",
      "description": "Required; trimmed. Blank after trimming, or longer than 255 characters, is a 400. A name a folder in the same place already has, ignoring case, is a 409 `NAME_TAKEN`."
     },
     "parentId": {
      "type": "string",
      "format": "uuid",
      "description": "Optional parent. Omit or send null for a root folder. Must exist in the same workspace — an unknown or cross-tenant id is a 400 \"Parent folder not found.\""
     }
    }
   },
   "FolderUpdateRequest": {
    "type": "object",
    "required": [
     "name"
    ],
    "properties": {
     "name": {
      "type": "string",
      "description": "Required; trimmed. Blank, or longer than 255 characters, is a 400. A name another folder in the new place already has, ignoring case, is a 409 `NAME_TAKEN`."
     },
     "parentId": {
      "type": "string",
      "format": "uuid",
      "description": "The new parent, applied EXACTLY as sent. Omitting it (or sending null) MOVES THE FOLDER TO THE ROOT — to rename without moving, re-send the folder's current `parentId`. Cannot be the folder itself or one of its descendants."
     }
    }
   },
   "FolderMutationAck": {
    "type": "object",
    "description": "Bare acknowledgement returned by folder update and delete. It does NOT contain the folder — re-list if you need the new state.",
    "required": [
     "id"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid",
      "description": "The folder the operation applied to."
     },
     "updated": {
      "type": "boolean",
      "description": "Always true; present on the update response only."
     },
     "deleted": {
      "type": "boolean",
      "description": "Always true; present on the delete response only."
     }
    }
   },
   "TemplateResource": {
    "type": "object",
    "description": "A template with its definition: an immutable snapshot of a workflow's draft taken when it was saved (or its steps last updated). A copy, never a live link: workflows made from it are not affected by changing or deleting it.",
    "required": [
     "id",
     "scope",
     "name",
     "description",
     "category",
     "triggerType",
     "createdAt",
     "updatedAt",
     "readable",
     "author",
     "source",
     "savedHere",
     "savedIn",
     "sourceWorkflow",
     "trigger",
     "marks",
     "steps",
     "whenLine",
     "slots",
     "toFill",
     "available",
     "unavailable",
     "uses",
     "canManage",
     "definition"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "scope": {
      "type": "string",
      "enum": [
       "org",
       "workspace",
       "installation"
      ],
      "description": "Who sees it: `org` — an organization's, every workspace of it; `workspace` — a customer account's, the workspace it was saved in only; `installation` — shared by the installation with its customer accounts (listed only in a customer account, and only while the license has embedding), read-only here."
     },
     "key": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",
      "description": "An installation template's stable handle. Absent on every other template."
     },
     "name": {
      "type": "string"
     },
     "description": {
      "type": "string",
      "description": "What it does, as saved. May be empty: `whenLine` then describes it."
     },
     "category": {
      "type": "string",
      "description": "Free-text grouping label; empty when none."
     },
     "triggerType": {
      "type": "string"
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "updatedAt": {
      "type": "string",
      "format": "date-time"
     },
     "publishedAt": {
      "type": "string",
      "format": "date-time",
      "description": "When an installation template was first offered. Absent on every other template."
     },
     "readable": {
      "type": "boolean",
      "description": "false: its definition cannot be read. Such a row is listed only to someone who may delete it (`canManage`), with everything derived from the definition empty."
     },
     "author": {
      "type": [
       "object",
       "null"
      ],
      "description": "Who saved it; null for an installation template.",
      "required": [
       "id",
       "name",
       "left"
      ],
      "properties": {
       "id": {
        "type": [
         "string",
         "null"
        ],
        "format": "uuid"
       },
       "name": {
        "type": [
         "string",
         "null"
        ],
        "description": "Their name, else their email; null once the account is deleted."
       },
       "left": {
        "type": "boolean",
        "description": "They are no longer a member of this organization."
       }
      }
     },
     "source": {
      "type": [
       "object",
       "null"
      ],
      "description": "An installation template's origin; null otherwise.",
      "required": [
       "organization"
      ],
      "properties": {
       "organization": {
        "type": "string",
        "description": "The organization it was shared from; empty once that organization is gone."
       }
      }
     },
     "savedHere": {
      "type": "boolean",
      "description": "It was saved in this workspace. Otherwise its bound values arrive blank (see `TemplateFillField`)."
     },
     "savedIn": {
      "type": [
       "string",
       "null"
      ],
      "description": "The workspace it was saved in (an installation template: the organization it was shared from); null when not recorded."
     },
     "sourceWorkflow": {
      "type": [
       "object",
       "null"
      ],
      "description": "The workflow it was saved (or last updated) from, while that workflow exists and is in this workspace.",
      "required": [
       "id",
       "name"
      ],
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "name": {
        "type": "string"
       }
      }
     },
     "trigger": {
      "type": [
       "object",
       "null"
      ],
      "required": [
       "name",
       "displayName",
       "pieceId",
       "actionId"
      ],
      "properties": {
       "name": {
        "type": "string"
       },
       "displayName": {
        "type": "string"
       },
       "pieceId": {
        "type": "string"
       },
       "actionId": {
        "type": "string"
       }
      }
     },
     "marks": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "App ids in run order: the trigger's, then one per step (branch steps `branch`, loops `loop`, a Code step `code`), never de-duplicated."
     },
     "steps": {
      "type": "array",
      "description": "Every step, in the order of `marks` after the trigger.",
      "items": {
       "type": "object",
       "required": [
        "name",
        "displayName",
        "pieceId",
        "actionId",
        "type",
        "depth"
       ],
       "properties": {
        "name": {
         "type": "string"
        },
        "displayName": {
         "type": "string"
        },
        "pieceId": {
         "type": "string"
        },
        "actionId": {
         "type": "string"
        },
        "type": {
         "type": "string",
         "enum": [
          "PIECE",
          "CODE",
          "BRANCH",
          "LOOP"
         ]
        },
        "depth": {
         "type": "integer",
         "description": "0 at the top level; one more inside a branch arm, a loop body or an error path."
        }
       }
      }
     },
     "whenLine": {
      "type": "string",
      "description": "A generated line for a template without a description: how it starts, then its first three steps' names."
     },
     "slots": {
      "type": "array",
      "description": "The connections a workflow made from it needs, one per service account.",
      "items": {
       "type": "object",
       "required": [
        "key",
        "pieceId",
        "pieceName",
        "steps",
        "stepLabels",
        "connectionId"
       ],
       "properties": {
        "key": {
         "type": "string"
        },
        "pieceId": {
         "type": "string",
         "description": "The connection's app (what `GET /api/connections` rows carry)."
        },
        "pieceName": {
         "type": "string"
        },
        "steps": {
         "type": "array",
         "items": {
          "type": "string"
         }
        },
        "stepLabels": {
         "type": "array",
         "items": {
          "type": "string"
         }
        },
        "connectionId": {
         "type": [
          "string",
          "null"
         ],
         "description": "The connection the template's own steps name; null for an installation template or none."
        }
       }
      }
     },
     "toFill": {
      "type": "integer",
      "description": "How many values using it here will ask for: required fields left blank and bound values blanked here."
     },
     "available": {
      "type": "boolean"
     },
     "unavailable": {
      "type": [
       "object",
       "null"
      ],
      "description": "Why it cannot be used on this installation.",
      "required": [
       "code",
       "message"
      ],
      "properties": {
       "code": {
        "type": "string",
        "enum": [
         "PIECE_UNAVAILABLE",
         "STEP_INCOMPLETE",
         "ACTION_UNAVAILABLE",
         "UNREADABLE"
        ]
       },
       "message": {
        "type": "string"
       },
       "step": {
        "type": "string",
        "description": "The step it is about, by name."
       }
      }
     },
     "uses": {
      "type": [
       "object",
       "null"
      ],
      "description": "Workflows of this organization made from it, and how many are live. Sent only to its author, an organization admin or a platform admin.",
      "required": [
       "workflows",
       "live"
      ],
      "properties": {
       "workflows": {
        "type": "integer"
       },
       "live": {
        "type": "integer"
       }
      }
     },
     "canManage": {
      "type": "boolean",
      "description": "The caller may rename, update the steps of and delete it: its creator, an organization admin, or for a `workspace` template that workspace's admin."
     },
     "createdBy": {
      "type": "string",
      "format": "uuid",
      "description": "Who saved it, as before the Templates redesign (`author` names them). Absent on an installation template and when the account is gone."
     },
     "definition": {
      "$ref": "#/components/schemas/TemplateFlowDefinition"
     },
     "blanked": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TemplateBlankedValue"
      },
      "description": "Only on a save's and an update of the steps' answers: each value the copy left out (a credential typed into a step)."
     }
    }
   },
   "TemplateFlowDefinition": {
    "type": "object",
    "description": "A workflow definition — the same document the workflow API uses (`schemaVersion` is 2). It is a TREE, not a list: `trigger` is the root and each node chains onward through `nextAction`; `branches[].firstAction` and `firstLoopAction` hold nested sub-chains, and `onErrorAction` holds a step's error arm. A step's settings live in `config`, never `settings`. The trigger names its trigger in a field called `actionId`, and trigger `type` is lowercase while action `type` is UPPERCASE (`PIECE`, `BRANCH`, `LOOP`, `CODE`). A step that uses credentials carries `connectionId` — a bare connection id, workspace-scoped, which is why it may be cleared when the template is used in another workspace.",
    "additionalProperties": true,
    "properties": {
     "schemaVersion": {
      "type": "integer",
      "enum": [
       2
      ],
      "description": "Always 2."
     },
     "trigger": {
      "type": "object",
      "additionalProperties": true,
      "description": "The root of the tree; its `nextAction` begins the chain of actions."
     },
     "variables": {
      "type": "object",
      "additionalProperties": true,
      "description": "Plain (non-secret) workflow variables. Sensitive variables live OUTSIDE the definition, so a template can never carry one."
     },
     "settings": {
      "type": "object",
      "additionalProperties": true,
      "description": "Workflow-level settings (timeout, concurrency limit, error workflow, …)."
     }
    }
   },
   "TemplateCreateRequest": {
    "type": "object",
    "required": [
     "workflowId"
    ],
    "properties": {
     "workflowId": {
      "type": "string",
      "format": "uuid",
      "description": "The workflow whose CURRENT DRAFT definition is snapshotted. Must be in the caller's workspace. You never submit a definition yourself."
     },
     "name": {
      "type": "string",
      "maxLength": 255,
      "description": "Optional; trimmed. Blank falls back to the source workflow's name. Max 255 characters."
     },
     "description": {
      "type": "string",
      "description": "Optional; trimmed. Blank stays blank (the template then shows a generated `whenLine`)."
     },
     "category": {
      "type": "string",
      "description": "Optional free-text grouping label; trimmed. No server-side vocabulary."
     }
    }
   },
   "TemplateUpdateRequest": {
    "type": "object",
    "description": "Only these three presentation fields are updatable here; the definition is replaced only by `PUT /api/templates/{id}/definition`. Each field is applied as sent, so omitting `description` or `category` CLEARS it.",
    "required": [
     "name"
    ],
    "properties": {
     "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 255,
      "description": "Required, and not blank: a name that is empty after trimming is refused with 400 `VALIDATION_ERROR`, as is one over 255 characters."
     },
     "description": {
      "type": "string",
      "description": "Trimmed. Omitting it clears the stored description."
     },
     "category": {
      "type": "string",
      "description": "Trimmed. Omitting it clears the stored category."
     }
    }
   },
   "TemplateUseRequest": {
    "type": "object",
    "description": "Optional body. Send nothing at all to make a draft named after the template, with each connection its steps named that resolves here.",
    "properties": {
     "name": {
      "type": "string",
      "maxLength": 255,
      "description": "Optional name for the new workflow; trimmed. Blank or absent uses the template's name. Max 255 characters."
     },
     "connections": {
      "type": "object",
      "additionalProperties": {
       "type": "string",
       "format": "uuid"
      },
      "description": "Slot key → a connection of this workspace made for the slot's service.",
      "example": {
       "c1": "0b5e3a52-0c0e-4d2b-9a66-0f3c6b1f3a10"
      }
     },
     "publish": {
      "type": "boolean",
      "default": false,
      "description": "Turn the new workflow on when it can run. Needs `workflow:publish`. A refused turn-on keeps the draft and is reported in `install.publishRefusal`."
     },
     "folderId": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "A folder of this workspace to file the new workflow in."
     },
     "values": {
      "$ref": "#/components/schemas/TemplateValues"
     }
    }
   },
   "TemplateCreatedWorkflow": {
    "type": "object",
    "description": "The workflow created from a template — the same shape the workflows API returns — and what the install did.",
    "required": [
     "id",
     "name",
     "status",
     "triggerType",
     "definition",
     "install"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string"
     },
     "description": {
      "type": "string",
      "description": "Copied from the template."
     },
     "definition": {
      "allOf": [
       {
        "$ref": "#/components/schemas/TemplateFlowDefinition"
       }
      ],
      "description": "The template's snapshot with its connection slots filled (steps of a slot left empty, and any other connection this workspace cannot resolve, carry no connection), and previously-unpinned steps on the fleet's current app versions."
     },
     "status": {
      "type": "string",
      "enum": [
       "draft",
       "active",
       "paused"
      ],
      "description": "`draft`, or `active` when `publish` turned it on."
     },
     "triggerType": {
      "type": "string",
      "description": "Copied from the template."
     },
     "webhookToken": {
      "type": "string",
      "description": "Present for a webhook workflow that `publish` turned on: the secret segment of the public webhook URL. Treat it as a credential."
     },
     "folderId": {
      "type": "string",
      "format": "uuid",
      "description": "The folder it was filed in, when `folderId` was sent."
     },
     "tags": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "hasUnpublishedChanges": {
      "type": "boolean"
     },
     "workspaceId": {
      "type": "string",
      "format": "uuid"
     },
     "orgId": {
      "type": "string",
      "format": "uuid"
     },
     "createdBy": {
      "type": "string",
      "format": "uuid"
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "updatedAt": {
      "type": "string",
      "format": "date-time"
     },
     "install": {
      "$ref": "#/components/schemas/TemplateInstall"
     },
     "latestVersionId": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "Present when `publish` turned it on: the published version."
     },
     "definitionRev": {
      "type": "integer",
      "format": "int64",
      "description": "Present when `publish` turned it on."
     },
     "testPayloads": {
      "type": [
       "object",
       "null"
      ],
      "additionalProperties": true,
      "description": "Present, null, when `publish` turned it on."
     }
    }
   },
   "TemplateInstall": {
    "type": "object",
    "description": "What using the template did, beside the workflow it made.",
    "required": [
     "published",
     "publishRefusal",
     "missing"
    ],
    "properties": {
     "published": {
      "type": "boolean",
      "description": "The new workflow was turned on."
     },
     "publishRefusal": {
      "type": [
       "object",
       "null"
      ],
      "required": [
       "code",
       "message"
      ],
      "properties": {
       "code": {
        "type": "string"
       },
       "message": {
        "type": "string"
       }
      },
      "description": "Why `publish` did not publish it: `CONNECTION_NOT_WORKING` when a chosen connection is `error`, `expired` or `pending` (the workflow is made as a draft), else the `{code, message}` `POST /api/workflows/{id}/publish` answers (`VALIDATION_ERROR`, `PIECE_NOT_ALLOWED`, `PIECES_UNAVAILABLE`). null when it did, or was not asked."
     },
     "missing": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/EmbedTemplateSlot"
      },
      "description": "Slots left without a connection."
     }
    }
   },
   "TemplateUsePreviewRequest": {
    "type": "object",
    "description": "Optional body: the connections and values the person has chosen so far.",
    "properties": {
     "connections": {
      "type": "object",
      "additionalProperties": {
       "type": "string",
       "format": "uuid"
      },
      "description": "Slot key → a connection of this workspace made for the slot's service.",
      "example": {
       "c1": "0b5e3a52-0c0e-4d2b-9a66-0f3c6b1f3a10"
      }
     },
     "values": {
      "$ref": "#/components/schemas/TemplateValues"
     }
    }
   },
   "TemplateUsePreview": {
    "type": "object",
    "required": [
     "slots",
     "diagnostics",
     "canPublish",
     "publishBlockedBy",
     "fill",
     "savedHere",
     "savedIn"
    ],
    "properties": {
     "slots": {
      "type": "array",
      "items": {
       "type": "object",
       "required": [
        "key",
        "pieceId",
        "pieceName",
        "steps",
        "candidates",
        "selected"
       ],
       "properties": {
        "key": {
         "type": "string",
         "description": "The slot (`c1`, `c2`, … in the order the steps first use it)."
        },
        "pieceId": {
         "type": "string",
         "description": "The service a connection for it must be for."
        },
        "pieceName": {
         "type": "string"
        },
        "steps": {
         "type": "array",
         "items": {
          "type": "object",
          "required": [
           "name",
           "displayName"
          ],
          "properties": {
           "name": {
            "type": "string",
            "description": "`trigger` for the trigger."
           },
           "displayName": {
            "type": "string"
           }
          }
         }
        },
        "candidates": {
         "type": "array",
         "description": "This workspace's connections for the service; those needing a new sign-in (`expired`, `error`) last.",
         "items": {
          "type": "object",
          "required": [
           "id",
           "name",
           "status",
           "managedBy"
          ],
          "properties": {
           "id": {
            "type": "string",
            "format": "uuid"
           },
           "name": {
            "type": "string"
           },
           "status": {
            "type": "string"
           },
           "managedBy": {
            "type": [
             "string",
             "null"
            ],
            "enum": [
             "partner",
             null
            ]
           }
          }
         }
        },
        "selected": {
         "type": [
          "string",
          "null"
         ],
         "format": "uuid",
         "description": "The connection that fills it now: the one chosen, else the one the template's steps named where it is a candidate and works (never one that is `error`, `expired` or `pending`)."
        }
       }
      }
     },
     "diagnostics": {
      "type": "array",
      "description": "What `POST /api/workflows/validate` would report on the workflow as it would be made, with this workspace's apps applied. Each with `stepLabel` (the step as the canvas names it) and `fieldLabel` (the app's title for the field; empty when it is not about an app's field).",
      "items": {
       "allOf": [
        {
         "$ref": "#/components/schemas/WorkflowDiagnostic"
        },
        {
         "type": "object",
         "required": [
          "stepLabel",
          "fieldLabel"
         ],
         "properties": {
          "stepLabel": {
           "type": "string"
          },
          "fieldLabel": {
           "type": "string"
          }
         }
        }
       ]
      }
     },
     "canPublish": {
      "type": "boolean",
      "description": "It could be published as it would be made: false also when a chosen connection is `error`, `expired` or `pending` (`publishBlockedBy.code: CONNECTION_NOT_WORKING`). Whether the caller may publish is the caller's own permissions."
     },
     "publishBlockedBy": {
      "type": [
       "object",
       "null"
      ],
      "required": [
       "code",
       "message"
      ],
      "properties": {
       "code": {
        "type": "string",
        "description": "The first blocking diagnostic's code."
       },
       "message": {
        "type": "string"
       },
       "step": {
        "type": "string",
        "description": "The step it is about, as the canvas names it."
       },
       "field": {
        "type": "string",
        "description": "The field it is about, as the app titles it."
       }
      },
      "description": "The first thing that keeps it from being turned on; null when nothing does."
     },
     "fill": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TemplateFillField"
      },
      "description": "What the person is asked to fill in, in step order: worked out before the request's `values`, so it stays the same while they type."
     },
     "savedHere": {
      "type": "boolean",
      "description": "The template was saved in this workspace."
     },
     "savedIn": {
      "type": [
       "string",
       "null"
      ],
      "description": "Where it was saved (a workspace's name; an installation template: the organization it was shared from)."
     }
    }
   },
   "MessageAck": {
    "type": "object",
    "description": "Bare acknowledgement payload returned by connection and template deletes.",
    "required": [
     "message"
    ],
    "properties": {
     "message": {
      "type": "string",
      "example": "Connection deleted"
     }
    }
   },
   "StatsSummaryBlock": {
    "type": "object",
    "description": "Run counts for one window. `successRate` = completed ÷ (completed + failed): cancelled runs are left out (someone stopped them, which says nothing about whether the workflow works; `cancelled` still counts them). Reported as 0 when nothing completed or failed in the window — render — when `total` is 0 rather than reading 0 as \"everything failed\". Releases up to v2026.10.5 counted cancelled runs as not worked. `avgDurationMs` averages COMPLETED runs only.",
    "required": [
     "total",
     "completed",
     "failed",
     "cancelled",
     "inFlight",
     "successRate",
     "avgDurationMs"
    ],
    "properties": {
     "total": {
      "type": "integer"
     },
     "completed": {
      "type": "integer"
     },
     "failed": {
      "type": "integer"
     },
     "cancelled": {
      "type": "integer"
     },
     "inFlight": {
      "type": "integer",
      "description": "Runs still pending/running/paused whose started_at falls in the window."
     },
     "successRate": {
      "type": "number",
      "minimum": 0,
      "maximum": 1
     },
     "avgDurationMs": {
      "type": "number"
     }
    }
   },
   "StatsOverview": {
    "type": "object",
    "required": [
     "window",
     "timeZone",
     "workflows",
     "runs",
     "limit",
     "series",
     "recent"
    ],
    "properties": {
     "window": {
      "type": "string",
      "enum": [
       "7d",
       "30d"
      ]
     },
     "timeZone": {
      "type": "string",
      "description": "The time zone the days were counted in: the `tz` asked for, or `UTC`."
     },
     "workflows": {
      "type": "object",
      "required": [
       "total",
       "active",
       "paused",
       "draft"
      ],
      "description": "The workspace's workflows: live (`active`), switched off (`paused`), never published (`draft`).",
      "properties": {
       "total": {
        "type": "integer"
       },
       "active": {
        "type": "integer"
       },
       "paused": {
        "type": "integer"
       },
       "draft": {
        "type": "integer"
       }
      }
     },
     "runs": {
      "type": "object",
      "required": [
       "window",
       "thisMonth"
      ],
      "properties": {
       "window": {
        "type": "object",
        "required": [
         "from",
         "total",
         "completed",
         "failed",
         "cancelled",
         "successRate"
        ],
        "description": "The workspace's production runs that started in the window, by status.",
        "properties": {
         "from": {
          "type": "string",
          "format": "date",
          "description": "The first day counted, in `timeZone`."
         },
         "total": {
          "type": "integer"
         },
         "completed": {
          "type": "integer"
         },
         "failed": {
          "type": "integer"
         },
         "cancelled": {
          "type": "integer"
         },
         "successRate": {
          "type": [
           "number",
           "null"
          ],
          "description": "Completed ÷ (completed + failed): cancelled runs are left out. Null when nothing completed or failed. Releases up to v2026.10.5 counted cancelled runs as not worked."
         }
        }
       },
       "thisMonth": {
        "type": "object",
        "required": [
         "from",
         "total",
         "completed",
         "failed",
         "asOf",
         "counted",
         "scope"
        ],
        "description": "The organization's production runs since the first of the month (UTC).",
        "properties": {
         "from": {
          "type": "string",
          "format": "date"
         },
         "total": {
          "type": "integer"
         },
         "completed": {
          "type": "integer"
         },
         "failed": {
          "type": "integer"
         },
         "asOf": {
          "type": [
           "string",
           "null"
          ],
          "format": "date-time",
          "description": "When the usage counts were last brought up to date; null when the installation keeps none."
         },
         "counted": {
          "type": "string",
          "enum": [
           "usage",
           "runs"
          ],
          "description": "`usage`: Platform › Usage's count, never lowered when runs are deleted. `runs`: the runs the organization holds now."
         },
         "scope": {
          "type": "string",
          "enum": [
           "organization"
          ]
         }
        }
       }
      }
     },
     "limit": {
      "type": "object",
      "required": [
       "concurrentRuns",
       "runningNow",
       "waiting",
       "scope"
      ],
      "description": "The organization's runs-at-once limit and its runs in the run line now. A held run is in neither count.",
      "properties": {
       "concurrentRuns": {
        "type": [
         "integer",
         "null"
        ],
        "description": "The limit in force (the organization's own, else the installation's default); null when there is none."
       },
       "runningNow": {
        "type": "integer"
       },
       "waiting": {
        "type": "integer",
        "description": "Runs accepted and waiting for a free slot."
       },
       "scope": {
        "type": "string",
        "enum": [
         "organization"
        ]
       }
      }
     },
     "series": {
      "type": "array",
      "description": "One point per day of the window in `timeZone`, oldest first.",
      "items": {
       "type": "object",
       "required": [
        "day",
        "completed",
        "failed",
        "cancelled",
        "total"
       ],
       "properties": {
        "day": {
         "type": "string",
         "format": "date"
        },
        "completed": {
         "type": "integer"
        },
        "failed": {
         "type": "integer"
        },
        "cancelled": {
         "type": "integer",
         "description": "Runs someone stopped that day: their own segment of a chart, never counted as worked or failed."
        },
        "total": {
         "type": "integer"
        }
       }
      }
     },
     "recent": {
      "type": "array",
      "description": "The workspace's latest 8 production runs, newest first.",
      "items": {
       "type": "object",
       "required": [
        "id",
        "workflow",
        "status",
        "trigger",
        "startedAt",
        "finishedAt",
        "durationMs"
       ],
       "properties": {
        "id": {
         "type": "string",
         "format": "uuid"
        },
        "workflow": {
         "type": "object",
         "required": [
          "id",
          "name"
         ],
         "properties": {
          "id": {
           "type": "string",
           "format": "uuid"
          },
          "name": {
           "type": "string"
          }
         }
        },
        "status": {
         "type": "string",
         "description": "The run's status, as `GET /api/runs` says it."
        },
        "waitingFor": {
         "type": "string",
         "enum": [
          "workflow",
          "organization"
         ],
         "description": "For a run waiting in line (status `waiting`): the limit it waits behind. Absent when it waits only for a free worker, and for every other run."
        },
        "claimedAt": {
         "type": "string",
         "format": "date-time",
         "description": "For a `pending` run a worker has taken: when. It is running."
        },
        "trigger": {
         "type": "string"
        },
        "startedAt": {
         "type": "string",
         "format": "date-time"
        },
        "finishedAt": {
         "type": [
          "string",
          "null"
         ],
         "format": "date-time"
        },
        "durationMs": {
         "type": [
          "integer",
          "null"
         ]
        }
       }
      }
     }
    }
   },
   "StatsSummary": {
    "type": "object",
    "required": [
     "window",
     "current",
     "previous",
     "activeWorkflows",
     "failingWorkflows",
     "topFailing"
    ],
    "properties": {
     "window": {
      "type": "string",
      "enum": [
       "24h",
       "7d",
       "30d"
      ]
     },
     "current": {
      "$ref": "#/components/schemas/StatsSummaryBlock"
     },
     "previous": {
      "$ref": "#/components/schemas/StatsSummaryBlock"
     },
     "previousCovered": {
      "type": "boolean",
      "description": "Whether the records hold the whole of the previous window: the workspace existed when it began, and run retention has not cut into it. When false, `previous` counts only part of that window and is no basis for a trend. Absent from servers before 2026-10-22."
     },
     "activeWorkflows": {
      "type": "integer",
      "description": "The workspace's ACTIVE (published) workflows right now — not windowed."
     },
     "failingWorkflows": {
      "type": "integer",
      "description": "How many DISTINCT workflows had at least one failed run in the window — the whole of what `topFailing` shows the top of."
     },
     "topFailing": {
      "type": "array",
      "description": "Up to 5 workflows with the most failed runs in the window, most-failing first. Workflows with zero failures are excluded; always `[]` rather than null.",
      "items": {
       "type": "object",
       "required": [
        "workflowId",
        "name",
        "failed",
        "total"
       ],
       "properties": {
        "workflowId": {
         "type": "string",
         "format": "uuid"
        },
        "name": {
         "type": "string"
        },
        "failed": {
         "type": "integer"
        },
        "total": {
         "type": "integer"
        }
       }
      }
     }
    }
   },
   "StatsTimeseries": {
    "type": "object",
    "required": [
     "window",
     "bucket",
     "points"
    ],
    "properties": {
     "window": {
      "type": "string",
      "enum": [
       "24h",
       "7d",
       "30d"
      ]
     },
     "bucket": {
      "type": "string",
      "enum": [
       "hour",
       "day"
      ]
     },
     "points": {
      "type": "array",
      "description": "A FIXED number of bucket-aligned points, the window divided by the bucket (by default 24 hourly for `24h`, 7 daily for `7d`, 30 daily for `30d`; `bucket=hour` on `7d` gives 168, `bucket=day` on `24h` one), the last one partial, gap-filled with zeros. Always `[]`, never null.",
      "items": {
       "type": "object",
       "required": [
        "t",
        "completed",
        "failed",
        "cancelled",
        "total"
       ],
       "properties": {
        "t": {
         "type": "string",
         "format": "date-time",
         "description": "The bucket's start, UTC."
        },
        "completed": {
         "type": "integer"
        },
        "failed": {
         "type": "integer"
        },
        "cancelled": {
         "type": "integer",
         "description": "Runs someone stopped in the bucket: their own segment of a chart, never counted as worked or failed."
        },
        "total": {
         "type": "integer"
        }
       }
      }
     }
    }
   },
   "EmbedError": {
    "type": "object",
    "required": [
     "error"
    ],
    "description": "The failure envelope. Branch on `error.code`.",
    "properties": {
     "error": {
      "type": "object",
      "required": [
       "code",
       "message"
      ],
      "properties": {
       "code": {
        "type": "string",
        "description": "A stable SCREAMING_SNAKE identifier."
       },
       "message": {
        "type": "string",
        "description": "A sentence for a person, in the installation's own words for workflows, runs, connections and templates. Never branch on it."
       },
       "details": {
        "type": "object",
        "additionalProperties": true,
        "description": "Keys depend on the code, for example `reason` (why a session ended), `switch` (which is off), `field` (which is wrong), `unknown`, `fields`, `notAllowed`, `invalid`, `value`, `origin`, `max`, `limit`, `slot`, `workflows`, `scope`, `role`, `expected` and `continueUrl`."
       }
      }
     }
    }
   },
   "EmbedCursorPagination": {
    "type": "object",
    "required": [
     "limit",
     "nextCursor"
    ],
    "properties": {
     "limit": {
      "type": "integer"
     },
     "nextCursor": {
      "type": [
       "string",
       "null"
      ],
      "description": "The `cursor` of the next page; null on the last page. A page that comes back full can carry a cursor even when nothing follows: the next page is then empty, with null here. Keep going until it is null."
     }
    }
   },
   "EmbedWorkspaceRef": {
    "type": "object",
    "required": [
     "id",
     "default"
    ],
    "properties": {
     "id": {
      "type": [
       "string",
       "null"
      ],
      "description": "The partner's workspace id; null for the default workspace."
     },
     "default": {
      "type": "boolean"
     }
    }
   },
   "EmbedAccount": {
    "type": "object",
    "required": [
     "id",
     "name",
     "status",
     "turnOff",
     "pieces",
     "assistant",
     "limits",
     "aiSignIn",
     "workspaces",
     "members",
     "suspendedAt",
     "suspendedReason",
     "deletedAt",
     "purgeAfter",
     "createdAt",
     "updatedAt",
     "organizationId"
    ],
    "properties": {
     "id": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "status": {
      "type": "string",
      "enum": [
       "active",
       "suspended",
       "deleted"
      ]
     },
     "turnOff": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "pieces": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "type": "string"
      },
      "description": "Null inherits the installation's list."
     },
     "assistant": {
      "type": "object",
      "required": [
       "monthlyTokens"
      ],
      "properties": {
       "monthlyTokens": {
        "type": [
         "integer",
         "null"
        ],
        "minimum": 0
       }
      }
     },
     "limits": {
      "type": "object",
      "required": [
       "concurrentRuns"
      ],
      "properties": {
       "concurrentRuns": {
        "type": [
         "integer",
         "null"
        ],
        "minimum": 0,
        "description": "How many runs this account may have executing at once. Null = the installation's default (Platform › Settings › General), 0 = no limit. Runs over it wait in line and start in order; none is refused for it."
       }
      }
     },
     "workspaces": {
      "type": "integer"
     },
     "members": {
      "type": "integer",
      "description": "Distinct people with access."
     },
     "suspendedAt": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "suspendedReason": {
      "type": [
       "string",
       "null"
      ]
     },
     "deletedAt": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "purgeAfter": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "updatedAt": {
      "type": "string",
      "format": "date-time"
     },
     "organizationId": {
      "type": "string",
      "format": "uuid",
      "description": "The installation's own id, for support."
     },
     "aiSignIn": {
      "type": "object",
      "required": [
       "address"
      ],
      "properties": {
       "address": {
        "type": [
         "string",
         "null"
        ],
        "format": "uri",
        "description": "This account's own AI app sign-in address: where an AI app's sign-in sends this account's people instead of the installation's address. Null: they go to the installation's address (Platform › What customers get › Developers). It is used only while its website is still one of the installation's allowed websites or one of this account's own; otherwise the installation's address is used, or, when the installation has none, no AI app can sign in to the account (`mcp.signInFrom` says which)."
       }
      }
     },
     "mcp": {
      "type": [
       "object",
       "null"
      ],
      "description": "Only on GET /accounts/{accountId}: the account's MCP addresses as its people find them on the MCP page. Null while Tokens or MCP is off for the account, or it is deleted. While an AI app can sign in to it (AI app sign-in on, and a sign-in address to send its people to: the account's own `aiSignIn.address` or the installation's), they are the account's own addresses and `signIn` is true; otherwise the plain addresses tokens use.",
      "required": [
       "build",
       "tools",
       "signIn"
      ],
      "properties": {
       "build": {
        "type": [
         "string",
         "null"
        ],
        "format": "uri",
        "description": "The address that builds and manages workflows; null while MCP building (`mcpBuild`) is off."
       },
       "tools": {
        "type": "string",
        "format": "uri",
        "description": "The address that calls the published workflows."
       },
       "signIn": {
        "type": "boolean",
        "description": "Whether an AI app can connect by signing in: through the account's own `aiSignIn.address` while its website is allowed, or the installation's AI app sign-in address."
       },
       "signInFrom": {
        "type": [
         "string",
         "null"
        ],
        "enum": [
         "account",
         "installation",
         null
        ],
        "description": "Whose address a sign-in uses: `account` (its own `aiSignIn.address`) or `installation`; null when `signIn` is false. `installation` while `aiSignIn.address` is set means its website is no longer allowed for the account."
       }
      }
     }
    }
   },
   "EmbedAccountInput": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200,
      "description": "Required to create. Left out or null: left as it is."
     },
     "turnOff": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "type": "string"
      },
      "description": "Switch ids off for this account, replacing the list; null or `[]` clears it. An id that does not exist is 400 `VALIDATION_ERROR` (`details.unknown`)."
     },
     "pieces": {
      "type": [
       "array",
       "null"
      ],
      "items": {
       "type": "string"
      },
      "maxItems": 2000,
      "description": "The account's own app list: app ids, within the installation's list; null inherits the installation's. An id no app has is 400 `VALIDATION_ERROR` (`details.field` `pieces`, `details.unknown`), one the installation's list leaves out the same with `details.notAllowed` — nothing is stored."
     },
     "assistant": {
      "type": "object",
      "properties": {
       "monthlyTokens": {
        "type": [
         "integer",
         "null"
        ],
        "minimum": 0
       }
      }
     },
     "limits": {
      "type": "object",
      "properties": {
       "concurrentRuns": {
        "type": [
         "integer",
         "null"
        ],
        "minimum": 0,
        "maximum": 1000000,
        "description": "The account's runs at once: a number, 0 for no limit, null for the installation's default. Left out, it is left as it is. The last change counts, from here or from Platform › Organizations."
       }
      }
     },
     "aiSignIn": {
      "type": [
       "object",
       "null"
      ],
      "description": "This account's own AI app sign-in address, for a partner whose people sign in on a domain of their own (a reseller's site). Left out, it is left as it is; null, `{\"address\": null}` or `{\"address\": \"\"}` clears it, and the account's people go to the installation's address again. `{}` and an address of spaces are refused (400). On a deleted account the PUT is 409 `ACCOUNT_DELETED`, even to clear it. Changing or clearing the address never affects AI apps already connected. An older server ignores this field on an existing account: check that the answer carries `aiSignIn`.",
      "required": [
       "address"
      ],
      "properties": {
       "address": {
        "type": [
         "string",
         "null"
        ],
        "maxLength": 2000,
        "description": "The page in your product where this account's people sign in. Surrounding spaces are trimmed. The installation's address's rules apply, and a few more:\n\n- https, or http on `localhost`, `127.0.0.1` or `[::1]`;\n- no `#fragment`, no user name or password, at most 2,000 characters, not the installation's own host;\n- no invisible or direction-changing characters; a host name or an IP address, with no trailing dot, and a valid port;\n- a query without `;`, where every `%` starts an escape, and with no parameter that starts with `gr_` in any letter case (`gr_request` is the installation's).\n\nA problem with the address itself is 400 `VALIDATION_ERROR` (`details.field` `aiSignIn.address`). It is stored and answered in a standard form: the host in lower case, an international name in its `xn--` form, no default port. Your query parameters are kept, but may be reordered and re-encoded when `gr_request` is added.\n\nIts website (scheme, host and port) must already be allowed for THIS account: one of the installation's allowed websites (a wildcard covers subdomains at any depth, never the bare domain) or exactly one of this account's own websites, added with `POST /accounts/{accountId}/websites` (a PUT there replaces the whole list), never another account's. Otherwise 422 `SIGN_IN_ADDRESS_NOT_ALLOWED` with `details.origin`. Either way nothing is stored, not even an account this PUT would have created: create the account with a `name`, add its website, then set the address. If that website is removed later, the stored address is kept but not used until the website is added again."
       }
      }
     }
    }
   },
   "EmbedToken": {
    "type": "object",
    "description": "An API token made in an embed: on the Tokens page by an editor, or by your server. Never the token itself.",
    "required": [
     "id",
     "name",
     "prefix",
     "access",
     "scopes",
     "surfaces",
     "workspaceId",
     "createdBy",
     "createdAt",
     "expiresAt",
     "lastUsedAt",
     "expiring",
     "turnedOff"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string"
     },
     "prefix": {
      "type": "string",
      "description": "The token's first characters (`grt_` and 8 more), to tell tokens apart."
     },
     "access": {
      "type": "string",
      "enum": [
       "read",
       "run",
       "build",
       "custom"
      ],
      "description": "The widest level its scopes reach: read; run (also start, cancel and re-run runs); build (also create and change workflows and connections). `custom`: it was made with `scopes` that are not exactly one of those levels."
     },
     "scopes": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "surfaces": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "api",
        "mcp"
       ]
      },
      "description": "Where it works: `api` (the App API) and `mcp` (AI apps, over MCP)."
     },
     "workspaceId": {
      "type": [
       "string",
       "null"
      ],
      "description": "The partner's id of its workspace; null for the account's default workspace."
     },
     "createdBy": {
      "type": "object",
      "required": [
       "kind"
      ],
      "properties": {
       "kind": {
        "type": "string",
        "enum": [
         "person",
         "partner"
        ]
       },
       "id": {
        "type": "string",
        "description": "The partner's user id of the editor who made it (kind person)."
       },
       "name": {
        "type": "string"
       }
      }
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time"
     },
     "lastUsedAt": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "expiring": {
      "type": "boolean",
      "description": "It stops within seven days."
     },
     "turnedOff": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Switches turned off for this token alone."
     }
    }
   },
   "EmbedTokenInput": {
    "type": "object",
    "required": [
     "name"
    ],
    "anyOf": [
     {
      "required": [
       "access"
      ]
     },
     {
      "required": [
       "scopes"
      ]
     }
    ],
    "description": "`name` and one of `access` or `scopes` are required; send `access` or `scopes`, not both. Neither is 400 `VALIDATION_ERROR` with `details.field` `access`: a token is given what it can do on purpose, never by default.",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100,
      "description": "How you will recognise it later."
     },
     "workspaceId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 255,
      "description": "The partner's workspace id; `default` names the account's default workspace. Omit for the default workspace. (An empty value here is read as omitted; the other routes that take `workspaceId` refuse it.)"
     },
     "access": {
      "type": "string",
      "enum": [
       "read",
       "run",
       "build"
      ],
      "description": "What it can do, each level including the one before; narrowed to what the account's switches allow."
     },
     "scopes": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "workflow:read",
        "workflow:write",
        "workflow:delete",
        "workflow:publish",
        "workflow:run",
        "connection:read",
        "connection:write",
        "connection:delete",
        "run:read",
        "run:manage"
       ]
      },
      "description": "Permissions one by one, instead of `access`. Repeats and blank items are dropped. Exactly the scopes of a level read as that `access`; anything else reads as `custom`."
     },
     "surfaces": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "api",
        "mcp"
       ]
      },
      "description": "Where it works. Omit for `[\"api\"]`; an empty list is refused. `mcp` needs the MCP switch."
     },
     "expiresInDays": {
      "type": "integer",
      "minimum": 7,
      "maximum": 365,
      "default": 90
     },
     "turnOff": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Switch ids to turn off for this token alone; it can never turn one on."
     }
    }
   },
   "EmbedWorkspace": {
    "type": "object",
    "required": [
     "id",
     "default",
     "name",
     "members",
     "createdAt",
     "workspaceId"
    ],
    "properties": {
     "id": {
      "type": [
       "string",
       "null"
      ],
      "description": "The partner's workspace id; null for the default workspace."
     },
     "default": {
      "type": "boolean"
     },
     "name": {
      "type": "string"
     },
     "members": {
      "type": "integer"
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "workspaceId": {
      "type": "string",
      "format": "uuid",
      "description": "The installation's own id of the workspace, for support. Elsewhere `workspaceId` is the partner's id (`id` here)."
     }
    }
   },
   "EmbedPerson": {
    "type": "object",
    "required": [
     "id",
     "name",
     "email"
    ],
    "properties": {
     "id": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "email": {
      "type": [
       "string",
       "null"
      ]
     }
    }
   },
   "EmbedMember": {
    "type": "object",
    "required": [
     "user",
     "workspace",
     "role",
     "createdAt"
    ],
    "properties": {
     "user": {
      "$ref": "#/components/schemas/EmbedPerson"
     },
     "workspace": {
      "$ref": "#/components/schemas/EmbedWorkspaceRef"
     },
     "role": {
      "type": "string",
      "enum": [
       "viewer",
       "operator",
       "editor"
      ]
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "EmbedMemberInput": {
    "type": "object",
    "required": [
     "role"
    ],
    "properties": {
     "role": {
      "type": "string",
      "enum": [
       "viewer",
       "operator",
       "editor"
      ]
     },
     "workspaceId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 255,
      "description": "Omitted = the account's default workspace (`default` names it too); the empty value is refused."
     },
     "user": {
      "$ref": "#/components/schemas/EmbedUserInput"
     }
    }
   },
   "EmbedUser": {
    "type": "object",
    "required": [
     "id",
     "name",
     "email",
     "memberships",
     "createdAt",
     "updatedAt"
    ],
    "properties": {
     "id": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "email": {
      "type": [
       "string",
       "null"
      ]
     },
     "memberships": {
      "type": "array",
      "items": {
       "type": "object",
       "required": [
        "account",
        "workspace",
        "role"
       ],
       "properties": {
        "account": {
         "type": "object",
         "required": [
          "id",
          "name"
         ],
         "properties": {
          "id": {
           "type": "string"
          },
          "name": {
           "type": "string"
          }
         }
        },
        "workspace": {
         "$ref": "#/components/schemas/EmbedWorkspaceRef"
        },
        "role": {
         "type": "string",
         "enum": [
          "viewer",
          "operator",
          "editor"
         ]
        }
       }
      }
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "updatedAt": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "EmbedUserInput": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200,
      "description": "Required to create. Left out or null: left as it is."
     },
     "email": {
      "type": [
       "string",
       "null"
      ],
      "maxLength": 320,
      "description": "Contact data only. Left out: left as it is. Null or empty clears it. Must look like an email address."
     }
    }
   },
   "EmbedLoginCodeRequest": {
    "type": "object",
    "required": [
     "user",
     "account",
     "role"
    ],
    "properties": {
     "user": {
      "type": "object",
      "required": [
       "id",
       "name"
      ],
      "properties": {
       "id": {
        "type": "string",
        "minLength": 1,
        "maxLength": 255
       },
       "name": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
       },
       "email": {
        "type": [
         "string",
         "null"
        ],
        "maxLength": 320
       }
      }
     },
     "account": {
      "type": "object",
      "required": [
       "id",
       "name"
      ],
      "properties": {
       "id": {
        "type": "string",
        "minLength": 1,
        "maxLength": 255
       },
       "name": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
       }
      }
     },
     "workspace": {
      "type": "object",
      "required": [
       "id"
      ],
      "description": "Omitted = the account's default workspace. `name` is needed the first time.",
      "properties": {
       "id": {
        "type": "string",
        "minLength": 1,
        "maxLength": 255
       },
       "name": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
       }
      }
     },
     "role": {
      "type": "string",
      "enum": [
       "viewer",
       "operator",
       "editor"
      ]
     },
     "turnOff": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Switches off for this login only. An id that does not exist is 400 `VALIDATION_ERROR` (`details.unknown`)."
     }
    }
   },
   "EmbedLoginCode": {
    "type": "object",
    "required": [
     "code",
     "expiresAt",
     "expiresIn",
     "account",
     "workspace",
     "user",
     "role"
    ],
    "properties": {
     "code": {
      "type": "string",
      "description": "`grl_…` — single use, 60 seconds."
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time"
     },
     "expiresIn": {
      "type": "integer"
     },
     "account": {
      "type": "object",
      "required": [
       "id",
       "status",
       "created"
      ],
      "properties": {
       "id": {
        "type": "string"
       },
       "status": {
        "type": "string",
        "enum": [
         "active",
         "suspended"
        ]
       },
       "created": {
        "type": "boolean"
       }
      }
     },
     "workspace": {
      "type": "object",
      "required": [
       "id",
       "default",
       "created"
      ],
      "properties": {
       "id": {
        "type": [
         "string",
         "null"
        ]
       },
       "default": {
        "type": "boolean"
       },
       "created": {
        "type": "boolean"
       }
      }
     },
     "user": {
      "type": "object",
      "required": [
       "id",
       "created"
      ],
      "properties": {
       "id": {
        "type": "string"
       },
       "created": {
        "type": "boolean"
       }
      }
     },
     "role": {
      "type": "string",
      "enum": [
       "viewer",
       "operator",
       "editor"
      ]
     }
    }
   },
   "EmbedBootstrap": {
    "type": "object",
    "required": [
     "protocol",
     "protocols",
     "available",
     "reason",
     "originAllowed",
     "allowedOrigins"
    ],
    "properties": {
     "protocol": {
      "type": "string",
      "const": "gorunner-embed/1"
     },
     "protocols": {
      "type": "array",
      "items": {
       "type": "integer"
      }
     },
     "available": {
      "type": "boolean"
     },
     "reason": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "not_licensed",
       "sign_in_off",
       null
      ]
     },
     "originAllowed": {
      "type": "boolean",
      "description": "Whether the page asking may host the embed: one of the installation's allowed websites, or a website the partner's server allowed for one customer."
     },
     "allowedOrigins": {
      "type": "array",
      "maxItems": 1,
      "items": {
       "type": "string"
      },
      "description": "The asking page's own origin when it is allowed; empty otherwise. Never the lists."
     }
    }
   },
   "EmbedWebsites": {
    "type": "object",
    "required": [
     "websites",
     "max"
    ],
    "properties": {
     "websites": {
      "type": "array",
      "items": {
       "type": "object",
       "required": [
        "origin"
       ],
       "properties": {
        "origin": {
         "type": "string"
        }
       }
      }
     },
     "max": {
      "type": "integer",
      "const": 200
     }
    }
   },
   "EmbedAccountWebsite": {
    "type": "object",
    "required": [
     "origin",
     "addedAt"
    ],
    "properties": {
     "origin": {
      "type": "string"
     },
     "addedAt": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "EmbedAccountWebsites": {
    "type": "object",
    "required": [
     "websites",
     "max"
    ],
    "properties": {
     "websites": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/EmbedAccountWebsite"
      }
     },
     "max": {
      "type": "integer",
      "const": 20
     }
    }
   },
   "EmbedSessionRequest": {
    "type": "object",
    "required": [
     "code",
     "parentOrigin"
    ],
    "properties": {
     "code": {
      "type": "string"
     },
     "parentOrigin": {
      "type": "string",
      "description": "The host page's origin. Missing or empty is 400 `VALIDATION_ERROR` (`details.field` `parentOrigin`) and the login code is NOT used, so a retry with it works."
     },
     "turnOff": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "replaces": {
      "type": "string",
      "description": "The `gri_…` token this one renews."
     }
    }
   },
   "EmbedSession": {
    "type": "object",
    "required": [
     "token",
     "expiresAt",
     "renewAfter",
     "renewInSeconds",
     "account",
     "workspace",
     "user",
     "role",
     "switches",
     "ignored"
    ],
    "properties": {
     "token": {
      "type": "string",
      "description": "`gri_…` — send as `Authorization: Bearer`; keep in memory only."
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time"
     },
     "renewAfter": {
      "type": "string",
      "format": "date-time"
     },
     "renewableUntil": {
      "type": "string",
      "format": "date-time",
      "description": "When renewing in the frame stops for this line of sessions (12 hours after the login code, the `EMBED_SESSION_MAX_HOURS` setting). Absent for a platform preview."
     },
     "renewInSeconds": {
      "type": "integer",
      "minimum": 0,
      "description": "`renewAfter` as whole seconds from this answer, by the server's clock: schedule the renewal from it, so a device whose clock is off still renews on time."
     },
     "account": {
      "type": "object",
      "required": [
       "id",
       "status"
      ],
      "properties": {
       "id": {
        "type": "string"
       },
       "status": {
        "type": "string"
       }
      }
     },
     "workspace": {
      "$ref": "#/components/schemas/EmbedWorkspaceRef"
     },
     "user": {
      "type": "object",
      "required": [
       "id"
      ],
      "properties": {
       "id": {
        "type": "string"
       }
      }
     },
     "role": {
      "type": "string",
      "enum": [
       "viewer",
       "operator",
       "editor"
      ]
     },
     "switches": {
      "type": "object",
      "additionalProperties": {
       "type": "boolean"
      },
      "description": "Every switch id with its effective value; a missing id is off."
     },
     "ignored": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "me": {
      "type": "object",
      "additionalProperties": true,
      "description": "What `GET /api/auth/me` answers this session."
     }
    }
   },
   "EmbedNarrowResult": {
    "type": "object",
    "required": [
     "switches",
     "ignored"
    ],
    "properties": {
     "switches": {
      "type": "object",
      "additionalProperties": {
       "type": "boolean"
      },
      "description": "Every switch id with its effective value; a missing id is off."
     },
     "ignored": {
      "type": "array",
      "items": {
       "type": "string"
      }
     }
    }
   },
   "EmbedTemplateSlot": {
    "type": "object",
    "required": [
     "key",
     "pieceId",
     "steps"
    ],
    "properties": {
     "key": {
      "type": "string",
      "description": "The slot's name in this template (`c1`, `c2`, … in the order the steps first use it)."
     },
     "pieceId": {
      "type": "string",
      "description": "The service a connection for this slot must be for."
     },
     "steps": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "The steps that use it; `trigger` for the trigger."
     }
    }
   },
   "EmbedTemplate": {
    "type": "object",
    "required": [
     "id",
     "key",
     "name",
     "description",
     "category",
     "triggerType",
     "pieces",
     "connections"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "key": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",
      "description": "The stable handle to install by."
     },
     "name": {
      "type": "string"
     },
     "description": {
      "type": "string"
     },
     "category": {
      "type": [
       "string",
       "null"
      ]
     },
     "triggerType": {
      "type": "string"
     },
     "pieces": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Every app its steps use."
     },
     "connections": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/EmbedTemplateSlot"
      },
      "description": "The connections a workflow made from it needs, one slot per service."
     }
    }
   },
   "EmbedConnection": {
    "type": "object",
    "required": [
     "key",
     "id",
     "name",
     "pieceId",
     "status",
     "managedBy",
     "workspace",
     "createdAt",
     "updatedAt"
    ],
    "description": "Never carries a secret.",
    "properties": {
     "key": {
      "type": [
       "string",
       "null"
      ],
      "description": "The partner's key; null for one the customer made."
     },
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string"
     },
     "pieceId": {
      "type": "string"
     },
     "status": {
      "type": "string",
      "enum": [
       "ok",
       "error",
       "expired"
      ],
      "description": "`expired`: its tokens can no longer be refreshed; replace them."
     },
     "managedBy": {
      "type": "string",
      "enum": [
       "partner",
       "customer"
      ]
     },
     "workspace": {
      "$ref": "#/components/schemas/EmbedWorkspaceRef"
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "updatedAt": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "EmbedConnectionInput": {
    "type": "object",
    "required": [
     "pieceId",
     "name",
     "values"
    ],
    "properties": {
     "pieceId": {
      "type": "string"
     },
     "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200,
      "description": "1–200 characters, the rule every name the partner gives follows: no control or invisible formatting characters."
     },
     "workspaceId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 255,
      "description": "Omitted = the account's default workspace (`default` names it too); the empty value is refused."
     },
     "authType": {
      "type": "string",
      "description": "One the app declares; omitted = the app's own."
     },
     "values": {
      "type": "object",
      "additionalProperties": true,
      "writeOnly": true,
      "description": "The credential. Write-only. An app's own auth fields: every value as text, even a number or a yes/no (`\"587\"`, `\"true\"`; anything else is 400 naming it, as is a required one left out); a key the app does not have is 400 `VALIDATION_ERROR` naming it in `details.field` (`values.<key>`), with `details.unknown` and the app's own `details.fields`; an app whose fields are all optional needs at least one. Or, for an app that signs in with OAuth2: `accessToken` (required), `refreshToken`, `expiresAt` (RFC 3339), `tokenType`, and — with a refresh token — the client that issued it: `clientId`/`clientSecret`, or `oauthApp`. Other keys are ignored for an OAuth2 app."
     },
     "oauthApp": {
      "type": "string",
      "enum": [
       "installation"
      ],
      "description": "The OAuth2 tokens came from the installation's app for this service: its client id is recorded, and every refresh uses that client. Refused when the installation has no app for the service, or with `clientId` in `values`."
     }
    }
   },
   "EmbedConnectionSignInInput": {
    "allOf": [
     {
      "$ref": "#/components/schemas/ConnectionOAuth2APIStartRequest"
     }
    ],
    "type": "object",
    "description": "The API sign-in's body, and the account's workspace. `connectionId` names a connection in that workspace; `returnUrl` may also be one of this account's own websites.",
    "properties": {
     "workspaceId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 255,
      "description": "The workspace the connection goes in (the partner's id; `default` names the default one). Omitted = the account's default workspace; the empty value is refused."
     }
    }
   },
   "EmbedWorkflow": {
    "type": "object",
    "required": [
     "id",
     "name",
     "description",
     "status",
     "triggerType",
     "workspace",
     "templateId",
     "createdAt",
     "updatedAt"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "name": {
      "type": "string"
     },
     "description": {
      "type": "string"
     },
     "status": {
      "type": "string",
      "enum": [
       "draft",
       "live",
       "paused"
      ],
      "description": "`draft`: never published; `live`: its trigger is on; `paused`: published, trigger off."
     },
     "triggerType": {
      "type": "string"
     },
     "workspace": {
      "$ref": "#/components/schemas/EmbedWorkspaceRef"
     },
     "templateId": {
      "type": [
       "string",
       "null"
      ],
      "format": "uuid",
      "description": "The template it was installed from."
     },
     "webhookUrl": {
      "type": "string",
      "format": "uri",
      "description": "Where this workflow takes deliveries (`POST /api/webhooks/{token}`): on a webhook-triggered workflow, once its first publish or save has made the address, and only in the two reads, `GET .../workflows` and `GET .../workflows/{workflowId}` (`Cache-Control: no-store`), never in a write's answer, which an `Idempotency-Key` can keep for a day. It takes deliveries while the workflow is `live` and answers 404 otherwise. It starts production runs with no other credential: hold it as you hold this key. It stays the same across enable and disable, and changes only when a token rotates it (`POST /api/workflows/{id}/webhook/rotate`). Absent otherwise."
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "updatedAt": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "EmbedWorkflowInstallInput": {
    "type": "object",
    "required": [
     "templateKey"
    ],
    "properties": {
     "templateKey": {
      "type": "string"
     },
     "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200,
      "description": "Omitted = the template's name."
     },
     "workspaceId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 255,
      "description": "Omitted = the account's default workspace (`default` names it too); the empty value is refused."
     },
     "connections": {
      "type": "object",
      "additionalProperties": {
       "oneOf": [
        {
         "type": "object",
         "required": [
          "key"
         ],
         "additionalProperties": false,
         "properties": {
          "key": {
           "type": "string",
           "minLength": 1,
           "maxLength": 255,
           "description": "The partner's connection key."
          }
         }
        },
        {
         "type": "object",
         "required": [
          "id"
         ],
         "additionalProperties": false,
         "properties": {
          "id": {
           "type": "string",
           "format": "uuid",
           "description": "A connection's id (one the customer made)."
          }
         }
        }
       ]
      },
      "description": "Slot `key` → the connection that fills it."
     },
     "enable": {
      "type": "boolean",
      "default": false
     }
    }
   },
   "EmbedInstalledWorkflow": {
    "allOf": [
     {
      "$ref": "#/components/schemas/EmbedWorkflow"
     },
     {
      "type": "object",
      "required": [
       "install"
      ],
      "properties": {
       "install": {
        "type": "object",
        "required": [
         "published",
         "publishRefusal",
         "missing"
        ],
        "properties": {
         "published": {
          "type": "boolean"
         },
         "publishRefusal": {
          "type": [
           "object",
           "null"
          ],
          "required": [
           "code",
           "message"
          ],
          "properties": {
           "code": {
            "type": "string"
           },
           "message": {
            "type": "string"
           }
          },
          "description": "Why `enable` could not publish (null when it did, or was not asked): the same `{code, message}` `POST …/enable` answers."
         },
         "missing": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/EmbedTemplateSlot"
          },
          "description": "Slots left without a connection."
         }
        }
       }
      }
     }
    ]
   },
   "EmbedRun": {
    "type": "object",
    "required": [
     "id",
     "workflowId",
     "workspace",
     "status",
     "trigger",
     "startedAt",
     "firstStartedAt",
     "finishedAt",
     "durationMs",
     "error",
     "failure"
    ],
    "description": "Never carries step data.",
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "workflowId": {
      "type": "string",
      "format": "uuid"
     },
     "workspace": {
      "$ref": "#/components/schemas/EmbedWorkspaceRef"
     },
     "status": {
      "type": "string",
      "enum": [
       "queued",
       "running",
       "held",
       "worked",
       "failed",
       "cancelled"
      ],
      "description": "`queued`: accepted, waiting for a worker or a free slot. `running`: a worker has it. `held`: waiting on a person or a timer."
     },
     "trigger": {
      "type": "string",
      "description": "What started it: the trigger's type (`webhook`, `cron`, `manual`, …)."
     },
     "startedAt": {
      "type": "string",
      "format": "date-time",
      "description": "When the run was accepted. It may then wait in line for a free slot before a worker starts it (`firstStartedAt`)."
     },
     "firstStartedAt": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "When a worker first started the run. `durationMs` counts from here, so the time from `startedAt` to this is the time the run waited in line. Null until a worker starts the run, and for a run started on v2026.10.5 or earlier or one that ran inline (an error handler's), whose `durationMs` counts from `startedAt`."
     },
     "finishedAt": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "durationMs": {
      "type": [
       "integer",
       "null"
      ],
      "description": "How long the run took, in milliseconds: from the first time a worker started the run (`firstStartedAt`, else `startedAt`) to its finish. Waits inside the run (a Delay, an Approval) count; the time it waited in the line for a free slot does not. A run cancelled while it waited has none. Null until it finishes."
     },
     "error": {
      "type": [
       "string",
       "null"
      ],
      "description": "Why it did not work, as a sentence: a failed run's failure, or `Cancelled by user` on a cancelled run (whoever cancelled it)."
     },
     "failure": {
      "type": [
       "object",
       "null"
      ],
      "description": "A failed run's error in the shape the `run.failed` event carries: the message (as `error`) and the first step that failed. Null for every other run.",
      "required": [
       "message",
       "step"
      ],
      "properties": {
       "message": {
        "type": "string"
       },
       "step": {
        "type": [
         "object",
         "null"
        ],
        "description": "The first step that failed; null when the run failed around its steps.",
        "required": [
         "name",
         "displayName"
        ],
        "properties": {
         "name": {
          "type": "string",
          "description": "The step's name in the definition (`step_2`)."
         },
         "displayName": {
          "type": "string"
         }
        }
       }
      }
     }
    }
   },
   "EmbedUsage": {
    "type": "object",
    "required": [
     "account",
     "from",
     "to",
     "runs",
     "testRuns",
     "assistantTokens",
     "asOf"
    ],
    "properties": {
     "account": {
      "type": "object",
      "required": [
       "id",
       "name"
      ],
      "properties": {
       "id": {
        "type": "string"
       },
       "name": {
        "type": "string"
       }
      }
     },
     "from": {
      "type": "string",
      "format": "date"
     },
     "to": {
      "type": "string",
      "format": "date"
     },
     "runs": {
      "type": "object",
      "required": [
       "worked",
       "failed",
       "held",
       "cancelled",
       "inProgress",
       "total"
      ],
      "description": "Production runs by the UTC day they started. `total` is the sum of the other five.",
      "properties": {
       "worked": {
        "type": "integer"
       },
       "failed": {
        "type": "integer"
       },
       "held": {
        "type": "integer"
       },
       "cancelled": {
        "type": "integer"
       },
       "inProgress": {
        "type": "integer"
       },
       "total": {
        "type": "integer"
       }
      }
     },
     "testRuns": {
      "type": "integer",
      "description": "Runs started from the editor to test, counted apart."
     },
     "assistantTokens": {
      "type": "integer",
      "description": "Assistant tokens the installation paid for."
     },
     "asOf": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "When the counts were last rolled up (every 5 minutes); null until the first count after the embed license arrives has finished."
     }
    }
   },
   "RunCursorPagination": {
    "type": "object",
    "description": "Paging facts in cursor mode: no total.",
    "required": [
     "limit",
     "nextCursor"
    ],
    "properties": {
     "limit": {
      "type": "integer",
      "description": "The page size applied."
     },
     "nextCursor": {
      "type": [
       "string",
       "null"
      ],
      "description": "Pass as `cursor` for the next page; null after the last."
     }
    }
   },
   "RunCounts": {
    "type": "object",
    "description": "The counts read: every figure the Runs page shows, under the list's filters.",
    "required": [
     "all",
     "worked",
     "failed",
     "cancelled",
     "held",
     "running",
     "queued",
     "failedWorkedOnRetry",
     "today",
     "latest",
     "range",
     "retentionDays",
     "tz"
    ],
    "properties": {
     "all": {
      "type": "integer",
      "description": "Runs that started in the range, every state."
     },
     "worked": {
      "type": "integer",
      "description": "In the range."
     },
     "failed": {
      "type": "integer",
      "description": "In the range."
     },
     "cancelled": {
      "type": "integer",
      "description": "In the range."
     },
     "held": {
      "type": "integer",
      "description": "Held on a person now, whatever their age."
     },
     "running": {
      "type": "integer",
      "description": "Running now (a Delay included), whatever their age."
     },
     "queued": {
      "type": "integer",
      "description": "Waiting in the run line now, whatever their age."
     },
     "failedWorkedOnRetry": {
      "type": "integer",
      "description": "Of `failed`, those a retry made good (they still count in `failed`)."
     },
     "failedTests": {
      "type": "integer",
      "description": "Only when `environment` is production: failed test runs in the range under the same filters."
     },
     "today": {
      "type": "object",
      "description": "Runs that started since local midnight in `tz`, whatever the range.",
      "properties": {
       "all": {
        "type": "integer"
       },
       "worked": {
        "type": "integer"
       },
       "failed": {
        "type": "integer"
       },
       "cancelled": {
        "type": "integer"
       },
       "from": {
        "type": "string",
        "format": "date-time"
       }
      }
     },
     "latest": {
      "type": [
       "object",
       "null"
      ],
      "description": "The newest run under the filters, in the range or open.",
      "properties": {
       "id": {
        "type": "string",
        "format": "uuid"
       },
       "workflowId": {
        "type": "string",
        "format": "uuid"
       },
       "workflowName": {
        "type": "string"
       },
       "startedAt": {
        "type": "string",
        "format": "date-time"
       },
       "state": {
        "type": "string",
        "enum": [
         "failed",
         "held",
         "running",
         "queued",
         "worked",
         "cancelled"
        ]
       }
      }
     },
     "range": {
      "type": "object",
      "properties": {
       "from": {
        "type": [
         "string",
         "null"
        ],
        "format": "date-time"
       },
       "to": {
        "type": [
         "string",
         "null"
        ],
        "format": "date-time"
       }
      }
     },
     "retentionDays": {
      "type": "integer",
      "description": "RUN_RETENTION_DAYS: 0 = runs are kept for ever. Past it a finished run is deleted, except each workflow's newest finished run per environment."
     },
     "tz": {
      "type": "string",
      "description": "The zone the days were counted in (UTC when absent or unknown)."
     },
     "days": {
      "type": "array",
      "description": "Only with `days=true`: each local day with runs in the range (days without runs are left out), newest first, at most 92, counting that day's runs in the range: the range's first and last days are often a part of a day (a 7-day window starts at this time 7 days ago), and count that part only; `from` is the day's own start.",
      "items": {
       "type": "object",
       "properties": {
        "all": {
         "type": "integer"
        },
        "worked": {
         "type": "integer"
        },
        "failed": {
         "type": "integer"
        },
        "cancelled": {
         "type": "integer"
        },
        "day": {
         "type": "string",
         "format": "date"
        },
        "from": {
         "type": "string",
         "format": "date-time"
        },
        "held": {
         "type": "integer"
        },
        "running": {
         "type": "integer"
        },
        "queued": {
         "type": "integer"
        }
       }
      }
     }
    }
   },
   "RunFailureGroup": {
    "type": "object",
    "description": "Failed runs of one workflow, failed step and reason class.",
    "required": [
     "key",
     "workflowId",
     "workflowName",
     "reason",
     "count",
     "firstAt",
     "lastAt",
     "latestRunId",
     "stillFailing",
     "workedSince",
     "retried",
     "retriedWorked",
     "retryOpen",
     "rerunnable",
     "rerunIds"
    ],
    "properties": {
     "key": {
      "type": "string",
      "description": "Opaque. `GET /api/runs?group={key}` lists the group's runs."
     },
     "workflowId": {
      "type": "string",
      "format": "uuid"
     },
     "workflowName": {
      "type": "string"
     },
     "workflowStatus": {
      "type": "string",
      "enum": [
       "active",
       "paused",
       "draft"
      ]
     },
     "stepKey": {
      "type": "string",
      "description": "Absent when the messages name no step."
     },
     "step": {
      "type": "string",
      "description": "The step's display name (published version, else draft, else key)."
     },
     "reason": {
      "type": "string",
      "description": "The newest failure's reason without the step prefix, at most 300 characters."
     },
     "count": {
      "type": "integer"
     },
     "firstAt": {
      "type": "string",
      "format": "date-time"
     },
     "lastAt": {
      "type": "string",
      "format": "date-time"
     },
     "latestRunId": {
      "type": "string",
      "format": "uuid"
     },
     "stillFailing": {
      "type": "boolean",
      "description": "The workflow is live and its newest finished production run failed in this group."
     },
     "workedSince": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time",
      "description": "When not still failing: the first production run that worked after `lastAt`."
     },
     "retried": {
      "type": "integer"
     },
     "retriedWorked": {
      "type": "integer"
     },
     "retryOpen": {
      "type": "integer"
     },
     "rerunnable": {
      "type": "integer",
      "description": "`count - retriedWorked - retryOpen`."
     },
     "rerunIds": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "uuid"
      },
      "description": "The oldest 200 rerunnable runs, oldest first."
     },
     "ranVersionNumber": {
      "type": "integer",
      "description": "The version the newest failure ran; absent for the draft."
     },
     "publishedVersionNumber": {
      "type": "integer"
     },
     "publishedAt": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "RunBulkRequest": {
    "type": "object",
    "required": [
     "ids"
    ],
    "properties": {
     "ids": {
      "type": "array",
      "minItems": 1,
      "maxItems": 200,
      "items": {
       "type": "string"
      },
      "description": "Run ids, in the order to act on them (send oldest first). Duplicates are taken once."
     }
    }
   },
   "RunBulkResult": {
    "type": "object",
    "required": [
     "results",
     "done",
     "refused"
    ],
    "properties": {
     "results": {
      "type": "array",
      "items": {
       "type": "object",
       "required": [
        "id",
        "ok"
       ],
       "properties": {
        "id": {
         "type": "string"
        },
        "ok": {
         "type": "boolean"
        },
        "runId": {
         "type": "string",
         "format": "uuid",
         "description": "Run again: the new run."
        },
        "status": {
         "type": "string",
         "description": "Cancel: the run's status now (`cancelled`, `cancelling`, or its finished status)."
        },
        "cancelled": {
         "type": "boolean",
         "description": "Cancel: whether this call stopped it."
        },
        "code": {
         "type": "string",
         "description": "Why it was refused: NOT_FOUND, ALREADY_RETRIED, RUN_NOT_FINISHED, WORKFLOW_NOT_LIVE, or the Run button's own code; for Cancel, the cancel's own code (such as `UNAVAILABLE`)."
        },
        "message": {
         "type": "string"
        }
       }
      },
      "description": "Each id's answer, in the order given; ids that are not UUIDs come last."
     },
     "done": {
      "type": "integer",
      "description": "Ids this call acted on: a run started, or a run stopped. A finished run left as it is (`ok: true, cancelled: false`) is counted neither here nor in `refused`."
     },
     "refused": {
      "type": "integer",
      "description": "Ids answered `ok: false`."
     }
    }
   },
   "RunRerunStarted": {
    "type": "object",
    "required": [
     "id",
     "retryOf",
     "status",
     "environment"
    ],
    "properties": {
     "id": {
      "type": "string",
      "format": "uuid"
     },
     "retryOf": {
      "type": "string",
      "format": "uuid"
     },
     "status": {
      "type": "string"
     },
     "environment": {
      "type": "string",
      "enum": [
       "production",
       "test"
      ]
     },
     "versionNumber": {
      "type": "integer",
      "description": "The version the new run executes; absent when it runs the draft."
     }
    }
   },
   "CodeStepUpgrade": {
    "type": "object",
    "description": "A Code step converted for Code 2.0.0 by `POST /api/code-steps/upgrade`.",
    "required": [
     "code",
     "inputs",
     "pieceVersion",
     "unchanged",
     "changes",
     "moreChanges",
     "notes",
     "moreNotes"
    ],
    "properties": {
     "code": {
      "type": "string",
      "description": "The new `config.code`."
     },
     "inputs": {
      "type": "object",
      "additionalProperties": true,
      "description": "The new `config.inputs`: the step's own rows in their order, then one row per new input, whose value is the original `{{ }}` text."
     },
     "pieceVersion": {
      "type": "string",
      "const": "2.0.0",
      "description": "The version to pin the step to."
     },
     "unchanged": {
      "type": "boolean",
      "description": "True when the code needed no rewrite and holds nothing to review (no `review` note): upgrading only changes the pin."
     },
     "changes": {
      "type": "array",
      "description": "The rewrites, in code order, at most 500.",
      "items": {
       "type": "object",
       "required": [
        "line",
        "before",
        "after"
       ],
       "properties": {
        "line": {
         "type": "integer",
         "description": "1-based line of the code."
        },
        "before": {
         "type": "string",
         "description": "The text replaced (cut at 400 characters)."
        },
        "after": {
         "type": "string",
         "description": "What replaced it (cut at 400 characters)."
        },
        "input": {
         "type": "string",
         "description": "The Inputs row it reads, when it reads one (several, comma-separated, for a string that held more than one `{{ }}`)."
        }
       }
      }
     },
     "moreChanges": {
      "type": "integer",
      "description": "How many rewrites were made beyond the 500 listed."
     },
     "notes": {
      "type": "array",
      "description": "What the author should know. `review` means look at that line yourself: the converter left something it could not convert safely. At most 500 notes about the code are listed (`moreNotes` counts the rest); after them always come the runtime differences no rewrite can see (NaN, BigInt, Map/Set, UTC, console, logs on failure) and \"Test the step\".",
      "items": {
       "type": "object",
       "required": [
        "level",
        "text"
       ],
       "properties": {
        "line": {
         "type": "integer",
         "description": "1-based line, when the note is about one."
        },
        "level": {
         "type": "string",
         "enum": [
          "info",
          "review"
         ]
        },
        "text": {
         "type": "string"
        }
       }
      }
     },
     "moreNotes": {
      "type": "integer",
      "description": "How many notes about the code there were beyond the 500 listed."
     }
    }
   },
   "DeferredValue": {
    "type": "object",
    "description": "A value this read left out because it is large: its size in bytes of JSON, as stored, and for a step's output its `head`.",
    "properties": {
     "bytes": {
      "type": "integer",
      "description": "The value's size in bytes of JSON."
     },
     "head": {
      "type": "object",
      "additionalProperties": true,
      "description": "A step OUTPUT's facts, so a client can say what the step did without reading it (an HTTP status, an approval's answer, a loop's count): the output's top-level keys — a scalar (up to 512 bytes) with its value, an object as its own scalar members, every other value null — at most 8 KB, scalars first. Absent for an input, the trigger data and an output that is not a JSON object. Never the output itself."
     }
    },
    "required": [
     "bytes"
    ]
   },
   "ReadyAnswer": {
    "type": "object",
    "required": [
     "status"
    ],
    "description": "`GET /api/ready`'s answer, without the `data` envelope.",
    "properties": {
     "status": {
      "type": "string",
      "enum": [
       "ok",
       "unavailable"
      ]
     },
     "version": {
      "type": "string"
     },
     "edition": {
      "type": "string"
     },
     "checks": {
      "type": "object",
      "additionalProperties": {
       "type": "object",
       "required": [
        "ok",
        "ms"
       ],
       "properties": {
        "ok": {
         "type": "boolean"
        },
        "ms": {
         "type": "integer",
         "description": "How long the check took, in milliseconds."
        },
        "error": {
         "type": "string",
         "description": "A fixed phrase, such as `connection refused`, when `ok` is false."
        }
       }
      }
     }
    }
   },
   "TriggerSubscription": {
    "type": "object",
    "description": "A workflow trigger's registration with its provider, for a vendor webhook trigger that registers itself on publish: `registering`, `active`, or `failed` with the provider's refusal in `lastError` and the next try in `nextAttemptAt`. A subscription that expires (`expiresAt`) is renewed by the workers before it does. The subscription's secret and the provider's registration ids are never in it.",
    "required": [
     "state",
     "piece"
    ],
    "properties": {
     "state": {
      "type": "string",
      "enum": [
       "registering",
       "active",
       "failed"
      ]
     },
     "piece": {
      "type": "string"
     },
     "lastError": {
      "type": "string"
     },
     "lastErrorAt": {
      "type": "string",
      "format": "date-time"
     },
     "nextAttemptAt": {
      "type": "string",
      "format": "date-time"
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time"
     },
     "registeredAt": {
      "type": "string",
      "format": "date-time"
     },
     "details": {
      "type": "object",
      "additionalProperties": {
       "type": "string"
      },
      "description": "What the registration listens to, by the provider's own ids — only safe facts, per app: GitHub `owner`/`repository`, GitLab `project`, Asana `resource`/`workspace`, ClickUp `team`, Jira `site`, Linear `teamId`, Trello `idModel`, Mailchimp `list`, Pipedrive `object`/`action`, Shopify `topic`, Calendly `scope`/`organization`/`user`, Typeform `formId`/`tag`, Tally and Jotform `formId`, Twilio `phoneNumber`, WhatsApp `phoneNumberId`/`businessAccountId`. Absent when there is none. Never a secret or an address that carries one."
     },
     "requestUrl": {
      "type": "string",
      "description": "For a trigger whose app sends every event to one address (Slack with your own Slack app): the Request URL to paste into the app's Event Subscriptions once. Not a secret: every delivery is verified with the app's Signing Secret."
     }
    }
   }
  },
  "headers": {
   "RunId": {
    "description": "The run this answer is about; read it with `GET /api/runs/{id}` (an API token with `run:read`). Also sent as `X-GoRunner-Run-Id`, with the same value.",
    "schema": {
     "type": "string",
     "format": "uuid"
    }
   },
   "RunStatus": {
    "description": "The run's status when the answer was written: `completed`, `failed` or `cancelled` once it ended; `pending`, `running`, `paused` or `waiting` while it goes on. Also sent as `X-GoRunner-Run-Status`, with the same value.",
    "schema": {
     "type": "string"
    }
   }
  }
 }
}
