{
  "components": {
    "responses": {
      "CustomToolsConflict": {
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/CustomToolsPublicProblem"
            }
          }
        },
        "description": "The request conflicts with current resource state."
      },
      "CustomToolsError": {
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/CustomToolsPublicProblem"
            }
          }
        },
        "description": "An error occurred (RFC 9457 problem+json). Switch on the stable `code`, not on prose."
      },
      "CustomToolsNotFound": {
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/CustomToolsPublicProblem"
            }
          }
        },
        "description": "The addressed resource does not exist, or is not visible to your tenant."
      },
      "CustomToolsPreconditionFailed": {
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/CustomToolsPublicProblem"
            }
          }
        },
        "description": "The supplied current-resource validator is stale. Refetch the affected resource, confirm the mutation is still desired, and retry with its new ETag."
      },
      "CustomToolsUnauthorized": {
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/CustomToolsPublicProblem"
            }
          }
        },
        "description": "API key missing or invalid."
      },
      "CustomToolsValidationProblem": {
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/CustomToolsPublicProblem"
            }
          }
        },
        "description": "The request was malformed or failed validation; see `errors` for the offending fields."
      },
      "BadRequest": {
        "description": "The request is valid HTTP but uses an unsupported parameter combination.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/PublicProblem"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "API key missing or invalid.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/PublicProblem"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The authenticated API key is not allowed to use this operation.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/PublicProblem"
            }
          }
        }
      },
      "NotFound": {
        "description": "The addressed resource does not exist, or is not visible to your tenant.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/PublicProblem"
            }
          }
        }
      },
      "Conflict": {
        "description": "The request conflicts with current resource state.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/PublicProblem"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "The operation could not be served. Read `retryable` in the response body before retrying: `true` means the same request can succeed later (honour `Retry-After` when present), `false` means it will not resolve on its own. When `retryable` is absent, treat the condition as transient and back off.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/PublicProblem"
            }
          }
        },
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying, sent only when a retry is expected to help. Its ABSENCE on a 503 is meaningful — check `retryable` in the body.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        }
      },
      "ValidationProblem": {
        "description": "The request was malformed or failed validation; see `errors` for the offending fields.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/PublicProblem"
            }
          }
        }
      },
      "Error": {
        "description": "An error occurred (RFC 9457 problem+json). Switch on the stable `code`, not on prose.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/PublicProblem"
            }
          }
        }
      }
    },
    "schemas": {
      "PublicBuildError": {
        "additionalProperties": false,
        "properties": {
          "code": {
            "title": "Code",
            "type": "string"
          },
          "message": {
            "title": "Message",
            "type": "string"
          }
        },
        "required": [
          "code",
          "message"
        ],
        "title": "PublicBuildError",
        "type": "object"
      },
      "PublicBuildEvent": {
        "additionalProperties": false,
        "properties": {
          "message": {
            "title": "Message",
            "type": "string"
          },
          "timestamp": {
            "description": "Event time as Unix milliseconds.",
            "title": "Timestamp",
            "type": "integer"
          }
        },
        "required": [
          "message",
          "timestamp"
        ],
        "title": "PublicBuildEvent",
        "type": "object"
      },
      "PublicBuildLogPage": {
        "additionalProperties": false,
        "properties": {
          "error": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PublicBuildError"
              },
              {
                "type": "null"
              }
            ]
          },
          "etag": {
            "description": "Opaque SDK-managed validator for the current Version snapshot.",
            "title": "Etag",
            "type": "string"
          },
          "items": {
            "items": {
              "$ref": "#/components/schemas/PublicBuildEvent"
            },
            "title": "Items",
            "type": "array"
          },
          "lifetimeEtag": {
            "description": "Opaque SDK-managed validator for the Tool lifetime that owns this Version.",
            "title": "Lifetimeetag",
            "type": "string"
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Pass as `cursor` for the next page. Null when there are no more events.",
            "title": "Nextcursor"
          },
          "status": {
            "$ref": "#/components/schemas/PublicVersionStatus"
          },
          "version": {
            "description": "The version these log events belong to, such as `v3`.",
            "title": "Version",
            "type": "string"
          }
        },
        "required": [
          "etag",
          "lifetimeEtag",
          "version",
          "status",
          "items",
          "nextCursor",
          "error"
        ],
        "title": "PublicBuildLogPage",
        "type": "object"
      },
      "PublicBuildResult": {
        "additionalProperties": false,
        "properties": {
          "action": {
            "description": "What happened: `build` started a new image build, `reuse_image` created a version from the current image, and `unchanged` returned the version already associated with this source.",
            "enum": [
              "build",
              "reuse_image",
              "unchanged"
            ],
            "title": "Action",
            "type": "string"
          },
          "version": {
            "$ref": "#/components/schemas/PublicVersion"
          }
        },
        "required": [
          "action",
          "version"
        ],
        "title": "PublicBuildResult",
        "type": "object"
      },
      "PublicCreateCustomToolRequest": {
        "additionalProperties": false,
        "properties": {
          "cpu": {
            "default": 1,
            "description": "Number of CPU cores allocated to each run. Defaults to 1.",
            "maximum": 8,
            "minimum": 1,
            "title": "Cpu",
            "type": "integer"
          },
          "description": {
            "default": "",
            "description": "Short explanation of the tool. Defaults to an empty string.",
            "maxLength": 2000,
            "title": "Description",
            "type": "string"
          },
          "displayName": {
            "default": "",
            "description": "Human-readable name shown in the UI. Defaults to an empty string.",
            "maxLength": 200,
            "title": "Displayname",
            "type": "string"
          },
          "gpuType": {
            "default": "None",
            "description": "GPU allocated to each run: \"None\", T4, L4, L40S, or A100. Defaults to \"None\". A10 is retired: it is still accepted and runs on L4.",
            "enum": [
              "None",
              "T4",
              "L4",
              "L40S",
              "A10",
              "A100"
            ],
            "title": "Gputype",
            "type": "string"
          },
          "memory": {
            "default": "8Gi",
            "description": "Memory allocated to each run. Defaults to \"8Gi\".",
            "enum": [
              "8Gi",
              "12Gi",
              "24Gi",
              "32Gi",
              "48Gi",
              "64Gi",
              "90Gi",
              "96Gi",
              "180Gi"
            ],
            "title": "Memory",
            "type": "string"
          },
          "name": {
            "description": "A unique lowercase name containing letters, numbers, and hyphens.",
            "maxLength": 50,
            "minLength": 3,
            "pattern": "^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$",
            "title": "Name",
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "title": "PublicCreateCustomToolRequest",
        "type": "object"
      },
      "PublicCreateVersionRequest": {
        "additionalProperties": false,
        "properties": {
          "expectedSourceDigest": {
            "anyOf": [
              {
                "pattern": "^sha256:[a-f0-9]{64}$",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Optional SHA-256 assertion, formatted as `sha256:<hex>`. The server always computes the digest. If supplied, a mismatch rejects the build. Without it, the server builds the uploaded bytes it reads.",
            "title": "Expectedsourcedigest"
          },
          "uploadId": {
            "description": "The `uploadId` returned when the source upload was created.",
            "maxLength": 200,
            "minLength": 1,
            "title": "Uploadid",
            "type": "string"
          }
        },
        "required": [
          "uploadId"
        ],
        "title": "PublicCreateVersionRequest",
        "type": "object"
      },
      "PublicCustomTool": {
        "additionalProperties": false,
        "properties": {
          "autoPublish": {
            "title": "Autopublish",
            "type": "boolean"
          },
          "canBuild": {
            "title": "Canbuild",
            "type": "boolean"
          },
          "canEdit": {
            "title": "Canedit",
            "type": "boolean"
          },
          "cpu": {
            "title": "Cpu",
            "type": "integer"
          },
          "createdAt": {
            "title": "Createdat",
            "type": "string"
          },
          "defaultVersion": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The version used by default, such as `v3`. Null when no version is published.",
            "title": "Defaultversion"
          },
          "description": {
            "title": "Description",
            "type": "string"
          },
          "displayName": {
            "title": "Displayname",
            "type": "string"
          },
          "estTime": {
            "title": "Esttime",
            "type": "string"
          },
          "etag": {
            "description": "Opaque SDK-managed validator for this Tool snapshot.",
            "title": "Etag",
            "type": "string"
          },
          "functions": {
            "items": {
              "type": "string"
            },
            "title": "Functions",
            "type": "array"
          },
          "gpuType": {
            "enum": [
              "None",
              "T4",
              "L4",
              "L40S",
              "A10",
              "A100"
            ],
            "title": "Gputype",
            "type": "string"
          },
          "hasSource": {
            "title": "Hassource",
            "type": "boolean"
          },
          "homeDiskGi": {
            "title": "Homediskgi",
            "type": "integer"
          },
          "maxRuntimeSeconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "description": "Maximum runtime for a tool run, in seconds. Null means no tool-specific limit.",
            "title": "Maxruntimeseconds"
          },
          "memory": {
            "enum": [
              "8Gi",
              "12Gi",
              "24Gi",
              "32Gi",
              "48Gi",
              "64Gi",
              "90Gi",
              "96Gi",
              "180Gi"
            ],
            "title": "Memory",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "paperUrl": {
            "title": "Paperurl",
            "type": "string"
          },
          "published": {
            "title": "Published",
            "type": "boolean"
          },
          "sourceDigest": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "SHA-256 digest of the current source archive after it has been admitted for a build. Null when the current source is unbuilt, absent, or hidden.",
            "title": "Sourcedigest"
          },
          "status": {
            "$ref": "#/components/schemas/PublicCustomToolStatus"
          },
          "tags": {
            "items": {
              "type": "string"
            },
            "title": "Tags",
            "type": "array"
          },
          "updatedAt": {
            "title": "Updatedat",
            "type": "string"
          }
        },
        "required": [
          "etag",
          "name",
          "displayName",
          "description",
          "functions",
          "status",
          "gpuType",
          "memory",
          "cpu",
          "homeDiskGi",
          "maxRuntimeSeconds",
          "hasSource",
          "sourceDigest",
          "published",
          "autoPublish",
          "estTime",
          "paperUrl",
          "tags",
          "defaultVersion",
          "createdAt",
          "updatedAt",
          "canEdit",
          "canBuild"
        ],
        "title": "PublicCustomTool",
        "type": "object"
      },
      "PublicCustomToolDetail": {
        "additionalProperties": false,
        "properties": {
          "autoPublish": {
            "title": "Autopublish",
            "type": "boolean"
          },
          "canBuild": {
            "title": "Canbuild",
            "type": "boolean"
          },
          "canEdit": {
            "title": "Canedit",
            "type": "boolean"
          },
          "cpu": {
            "title": "Cpu",
            "type": "integer"
          },
          "createdAt": {
            "title": "Createdat",
            "type": "string"
          },
          "defaultVersion": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The version used by default, such as `v3`. Null when no version is published.",
            "title": "Defaultversion"
          },
          "description": {
            "title": "Description",
            "type": "string"
          },
          "displayName": {
            "title": "Displayname",
            "type": "string"
          },
          "estTime": {
            "title": "Esttime",
            "type": "string"
          },
          "etag": {
            "description": "Opaque SDK-managed validator for this Tool snapshot.",
            "title": "Etag",
            "type": "string"
          },
          "functions": {
            "items": {
              "type": "string"
            },
            "title": "Functions",
            "type": "array"
          },
          "gpuType": {
            "enum": [
              "None",
              "T4",
              "L4",
              "L40S",
              "A10",
              "A100"
            ],
            "title": "Gputype",
            "type": "string"
          },
          "hasSource": {
            "title": "Hassource",
            "type": "boolean"
          },
          "homeDiskGi": {
            "title": "Homediskgi",
            "type": "integer"
          },
          "maxRuntimeSeconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "description": "Maximum runtime for a tool run, in seconds. Null means no tool-specific limit.",
            "title": "Maxruntimeseconds"
          },
          "memory": {
            "enum": [
              "8Gi",
              "12Gi",
              "24Gi",
              "32Gi",
              "48Gi",
              "64Gi",
              "90Gi",
              "96Gi",
              "180Gi"
            ],
            "title": "Memory",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "paperUrl": {
            "title": "Paperurl",
            "type": "string"
          },
          "published": {
            "title": "Published",
            "type": "boolean"
          },
          "sourceDigest": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "SHA-256 digest of the current source archive after it has been admitted for a build. Null when the current source is unbuilt, absent, or hidden.",
            "title": "Sourcedigest"
          },
          "status": {
            "$ref": "#/components/schemas/PublicCustomToolStatus"
          },
          "tags": {
            "items": {
              "type": "string"
            },
            "title": "Tags",
            "type": "array"
          },
          "updatedAt": {
            "title": "Updatedat",
            "type": "string"
          },
          "version": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PublicVersion"
              },
              {
                "type": "null"
              }
            ],
            "description": "The requested version, or the latest version when no version was requested."
          }
        },
        "required": [
          "etag",
          "name",
          "displayName",
          "description",
          "functions",
          "status",
          "gpuType",
          "memory",
          "cpu",
          "homeDiskGi",
          "maxRuntimeSeconds",
          "hasSource",
          "sourceDigest",
          "published",
          "autoPublish",
          "estTime",
          "paperUrl",
          "tags",
          "defaultVersion",
          "createdAt",
          "updatedAt",
          "canEdit",
          "canBuild",
          "version"
        ],
        "title": "PublicCustomToolDetail",
        "type": "object"
      },
      "PublicCustomToolPage": {
        "additionalProperties": false,
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/PublicCustomTool"
            },
            "title": "Items",
            "type": "array"
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Pass as `cursor` for the next page. Null when there are no more results.",
            "title": "Nextcursor"
          }
        },
        "required": [
          "items",
          "nextCursor"
        ],
        "title": "PublicCustomToolPage",
        "type": "object"
      },
      "PublicCustomToolStatus": {
        "enum": [
          "Draft",
          "Building",
          "Deployed"
        ],
        "title": "PublicCustomToolStatus",
        "type": "string"
      },
      "CustomToolsPublicProblem": {
        "additionalProperties": false,
        "description": "RFC 9457 problem detail. Serialized as `application/problem+json` on every public error.",
        "properties": {
          "code": {
            "description": "A stable machine-readable slug; switch on THIS, not prose.",
            "title": "Code",
            "type": "string"
          },
          "detail": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Instance-specific human explanation.",
            "title": "Detail"
          },
          "errors": {
            "anyOf": [
              {
                "items": {
                  "additionalProperties": true,
                  "type": "object"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Structured per-item detail (request-validation fields OR pipeline diagnostics).",
            "title": "Errors"
          },
          "retryable": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Whether the SAME request can succeed later. `true` means retry (honour `Retry-After` when present); `false` means this will not resolve on its own and retrying cannot change it. Omitted when the error does not specify — absence is NOT `false`.",
            "title": "Retryable"
          },
          "status": {
            "description": "The HTTP status code, duplicated in the body per RFC 9457.",
            "title": "Status",
            "type": "integer"
          },
          "title": {
            "description": "A short, human-readable summary of the error kind.",
            "title": "Title",
            "type": "string"
          },
          "type": {
            "description": "A URI identifying the error kind (dereferenceable docs).",
            "title": "Type",
            "type": "string"
          }
        },
        "required": [
          "type",
          "title",
          "status",
          "code"
        ],
        "title": "PublicProblem",
        "type": "object"
      },
      "PublicUpdateCustomToolRequest": {
        "additionalProperties": false,
        "properties": {
          "autoPublish": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "description": "Whether successful builds become the default version automatically. Omit or pass null to leave it unchanged.",
            "title": "Autopublish"
          },
          "cpu": {
            "anyOf": [
              {
                "maximum": 8,
                "minimum": 1,
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "description": "Number of CPU cores allocated to each run. Omit or pass null to leave it unchanged.",
            "title": "Cpu"
          },
          "description": {
            "anyOf": [
              {
                "maxLength": 2000,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Short explanation of the tool. Omit or pass null to leave it unchanged; pass an empty string to clear it.",
            "title": "Description"
          },
          "displayName": {
            "anyOf": [
              {
                "maxLength": 200,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Human-readable name shown in the UI. Omit or pass null to leave it unchanged; pass an empty string to clear it.",
            "title": "Displayname"
          },
          "estTime": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Estimated runtime in H:M:S format, for example \"0:1:0\". Minutes and seconds must be 0-59. Omit or pass null to leave it unchanged; pass an empty string to clear it.",
            "title": "Esttime"
          },
          "functions": {
            "anyOf": [
              {
                "items": {
                  "maxLength": 200,
                  "minLength": 1,
                  "type": "string"
                },
                "maxItems": 100,
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "description": "Short capability labels used for discovery. Omit or pass null to leave them unchanged; pass an empty list to clear them.",
            "title": "Functions"
          },
          "gpuType": {
            "anyOf": [
              {
                "enum": [
                  "None",
                  "T4",
                  "L4",
                  "L40S",
                  "A10",
                  "A100"
                ],
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "GPU allocated to each run: \"None\", T4, L4, L40S, or A100. Omit or pass null to leave it unchanged. A10 is retired: it is still accepted and runs on L4.",
            "title": "Gputype"
          },
          "homeDiskGi": {
            "anyOf": [
              {
                "maximum": 50,
                "minimum": 1,
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "description": "Writable home-disk capacity in GiB. Omit or pass null to leave it unchanged.",
            "title": "Homediskgi"
          },
          "memory": {
            "anyOf": [
              {
                "enum": [
                  "8Gi",
                  "12Gi",
                  "24Gi",
                  "32Gi",
                  "48Gi",
                  "64Gi",
                  "90Gi",
                  "96Gi",
                  "180Gi"
                ],
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Memory allocated to each run. Omit or pass null to leave it unchanged.",
            "title": "Memory"
          },
          "paperUrl": {
            "anyOf": [
              {
                "maxLength": 2000,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Related HTTP or HTTPS publication URL. Omit or pass null to leave it unchanged; pass an empty string to clear it.",
            "title": "Paperurl"
          },
          "tags": {
            "anyOf": [
              {
                "items": {
                  "maxLength": 200,
                  "minLength": 1,
                  "type": "string"
                },
                "maxItems": 100,
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "description": "Search and discovery tags. Omit or pass null to leave them unchanged; pass an empty list to clear them.",
            "title": "Tags"
          }
        },
        "title": "PublicUpdateCustomToolRequest",
        "type": "object"
      },
      "PublicUploadSession": {
        "additionalProperties": false,
        "properties": {
          "expiresAt": {
            "description": "When the upload URL expires, as an ISO 8601 timestamp.",
            "title": "Expiresat",
            "type": "string"
          },
          "maxBytes": {
            "description": "Maximum source-archive size, in bytes.",
            "title": "Maxbytes",
            "type": "integer"
          },
          "uploadHeaders": {
            "additionalProperties": {
              "type": "string"
            },
            "description": "Headers to include exactly as provided when uploading to `uploadUrl`.",
            "title": "Uploadheaders",
            "type": "object"
          },
          "uploadId": {
            "description": "Pass this value to the version-build request after uploading.",
            "title": "Uploadid",
            "type": "string"
          },
          "uploadMethod": {
            "const": "PUT",
            "description": "HTTP method to use with `uploadUrl`.",
            "title": "Uploadmethod",
            "type": "string"
          },
          "uploadUrl": {
            "description": "Short-lived URL for uploading the source archive.",
            "title": "Uploadurl",
            "type": "string"
          }
        },
        "required": [
          "uploadId",
          "uploadUrl",
          "uploadMethod",
          "uploadHeaders",
          "expiresAt",
          "maxBytes"
        ],
        "title": "PublicUploadSession",
        "type": "object"
      },
      "PublicVersion": {
        "additionalProperties": false,
        "properties": {
          "completedAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Completedat"
          },
          "createdAt": {
            "title": "Createdat",
            "type": "string"
          },
          "error": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PublicBuildError"
              },
              {
                "type": "null"
              }
            ]
          },
          "etag": {
            "description": "Opaque SDK-managed validator for this exact Version snapshot.",
            "title": "Etag",
            "type": "string"
          },
          "lifetimeEtag": {
            "description": "Opaque SDK-managed validator for the Tool lifetime that owns this Version.",
            "title": "Lifetimeetag",
            "type": "string"
          },
          "origin": {
            "title": "Origin",
            "type": "string"
          },
          "sourceDigest": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "SHA-256 digest of this version's source archive. Null when source is hidden.",
            "title": "Sourcedigest"
          },
          "sourceRevision": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Immutable source revision used by this version. Null when source is hidden.",
            "title": "Sourcerevision"
          },
          "startedAt": {
            "title": "Startedat",
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/PublicVersionStatus"
          },
          "terminal": {
            "description": "Whether the build has reached a final status and will not run again.",
            "title": "Terminal",
            "type": "boolean"
          },
          "version": {
            "description": "The version number, such as `v3`.",
            "title": "Version",
            "type": "string"
          }
        },
        "required": [
          "etag",
          "lifetimeEtag",
          "version",
          "sourceRevision",
          "sourceDigest",
          "status",
          "origin",
          "createdAt",
          "startedAt",
          "completedAt",
          "terminal",
          "error"
        ],
        "title": "PublicVersion",
        "type": "object"
      },
      "PublicVersionPage": {
        "additionalProperties": false,
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/PublicVersion"
            },
            "title": "Items",
            "type": "array"
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Pass as `cursor` for the next page. Null when there are no more results.",
            "title": "Nextcursor"
          }
        },
        "required": [
          "items",
          "nextCursor"
        ],
        "title": "PublicVersionPage",
        "type": "object"
      },
      "PublicVersionStatus": {
        "enum": [
          "Queued",
          "Running",
          "Complete",
          "Stopped"
        ],
        "title": "PublicVersionStatus",
        "type": "string"
      },
      "AssayOrderRequest": {
        "description": "An assay order. `POST /submit-assay`, `/validate-assay` and `/estimate-assay` all take this body.",
        "type": "object",
        "required": [
          "assayType"
        ],
        "additionalProperties": false,
        "properties": {
          "assayType": {
            "type": "string",
            "description": "Which assay to run. The options come from GET /assay-types.",
            "example": "adaptyv_affinity"
          },
          "modality": {
            "type": "string",
            "enum": [
              "antibody",
              "protein"
            ],
            "description": "Whether your sequences are antibodies or proteins. You can leave it out when the assay type only supports one."
          },
          "settings": {
            "type": "object",
            "description": "Options for this assay, such as the target. The assay type's `settings` list says what is needed.",
            "example": {
              "targetId": "<id from /assay-targets>",
              "method": "BLI",
              "antigenConcentrations": [
                1,
                10,
                100
              ]
            }
          },
          "sequences": {
            "type": "array",
            "maxItems": 5000,
            "description": "The sequences to test. For antibodies, send \"HEAVY:LIGHT\" strings (or just \"HEAVY\"), or objects with `name`, `heavy` and `light`. For proteins, send the sequence as a string (join chains with \":\"), or objects with `name` and `sequence`. A sequence you don't name is called seq_1, seq_2 and so on. Every name and every sequence has to be different.",
            "items": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "heavy": {
                      "type": "string"
                    },
                    "light": {
                      "type": "string"
                    },
                    "sequence": {
                      "type": "string"
                    }
                  }
                }
              ]
            }
          },
          "csvFile": {
            "type": "string",
            "description": "A CSV file of sequences, to use instead of `sequences`. Upload it first with PUT /upload/{filename}, then give its path within your files.",
            "example": "designs/round2.csv"
          },
          "columnMapping": {
            "type": "object",
            "description": "Which CSV columns hold what. Needed with `csvFile`. For antibodies, map `name`, `heavy` and `light`. For proteins, map `name` and `sequence`.",
            "properties": {
              "name": {
                "type": "string"
              },
              "heavy": {
                "type": "string"
              },
              "light": {
                "type": "string"
              },
              "sequence": {
                "type": "string"
              }
            }
          },
          "name": {
            "type": "string",
            "maxLength": 200,
            "description": "A name for the order so you can find it later."
          },
          "notes": {
            "type": "string",
            "maxLength": 2000,
            "description": "Anything the Tamarind team should know when reviewing the order."
          },
          "confirmDuplicate": {
            "type": "boolean",
            "default": false,
            "description": "Set to true to place an order that looks like one you placed recently. Use it after a `duplicate_order` response, when you do want a second, separately billed order."
          }
        }
      },
      "AssayOrder": {
        "type": "object",
        "description": "An assay order. You get one back when you submit, list or cancel orders.",
        "properties": {
          "assayId": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "assayType": {
            "type": "string",
            "nullable": true,
            "description": "Which assay the order is for. Null for some older orders placed in the web app."
          },
          "provider": {
            "type": "string",
            "description": "The lab running the order, for example adaptyv, ginkgo or aalpha. More labs may be added, so don't assume these are the only values."
          },
          "providerName": {
            "type": "string"
          },
          "assays": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "pending_quote",
              "pending_approval",
              "approved",
              "in_progress",
              "completed",
              "failed",
              "cancelling",
              "cancelled",
              "rejected",
              "unknown"
            ]
          },
          "sequenceCount": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "cost": {
            "type": "object",
            "nullable": true,
            "properties": {
              "amountUsd": {
                "type": "number"
              },
              "isEstimate": {
                "type": "boolean"
              },
              "invoiceUrl": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "cancellable": {
            "type": "boolean"
          },
          "resultsAvailable": {
            "type": "boolean",
            "description": "True once the lab has delivered results and GET /assay-results has files for you. It stays true from then on, even if the order's `status` changes later, so check this field instead of waiting for `status` to be `completed`."
          }
        }
      },
      "ClassicPublicProblem": {
        "additionalProperties": false,
        "description": "RFC 9457 problem detail. Serialized as `application/problem+json` on every public error.",
        "properties": {
          "code": {
            "description": "A stable machine-readable slug; switch on THIS, not prose.",
            "title": "Code",
            "type": "string"
          },
          "detail": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Instance-specific human explanation.",
            "title": "Detail"
          },
          "errors": {
            "anyOf": [
              {
                "items": {
                  "additionalProperties": true,
                  "type": "object"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Structured per-item detail (request-validation fields OR pipeline diagnostics).",
            "title": "Errors"
          },
          "status": {
            "description": "The HTTP status code, duplicated in the body per RFC 9457.",
            "title": "Status",
            "type": "integer"
          },
          "title": {
            "description": "A short, human-readable summary of the error kind.",
            "title": "Title",
            "type": "string"
          },
          "type": {
            "description": "A URI identifying the error kind (dereferenceable docs).",
            "title": "Type",
            "type": "string"
          }
        },
        "required": [
          "type",
          "title",
          "status",
          "code"
        ],
        "title": "PublicProblem",
        "type": "object"
      },
      "PublicToolRecommendationRequest": {
        "additionalProperties": false,
        "description": "Ask which tool to run for a stated task.",
        "properties": {
          "inputs": {
            "description": "Your input sequence components, to tailor the recommendation to your specific inputs. Optional, but they are what let the recommendation rest on a measurement of your own target rather than an overall ranking.",
            "items": {
              "$ref": "#/components/schemas/PublicRecommendationInput"
            },
            "maxItems": 32,
            "title": "Inputs",
            "type": "array"
          },
          "prompt": {
            "description": "What you want to do, in your own words — e.g. 'predict the structure of this antibody-antigen complex'.",
            "maxLength": 20000,
            "minLength": 1,
            "title": "Prompt",
            "type": "string"
          }
        },
        "required": [
          "prompt"
        ],
        "title": "PublicToolRecommendationRequest",
        "type": "object"
      },
      "PublicRecommendationInput": {
        "additionalProperties": false,
        "description": "One thing you already have. Give exactly one of `sequence`, `smiles`, or `ccd`.\n\nA protein chain is `sequence`. A small molecule is `smiles` or `ccd`, never both.",
        "properties": {
          "ccd": {
            "anyOf": [
              {
                "maxLength": 8,
                "minLength": 1,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "A ligand, as a PDB component id.",
            "title": "Ccd"
          },
          "id": {
            "anyOf": [
              {
                "maxLength": 200,
                "minLength": 1,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Optional label, used to point at this input in error messages.",
            "title": "Id"
          },
          "role": {
            "anyOf": [
              {
                "maxLength": 40,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Its part in the complex, in a word — e.g. `heavy`, `light`, `antigen`, `nanobody`, `target`, `enzyme` or `ligand`. Free text rather than a fixed list. Optional, and worth setting: it is what makes a request antibody-antigen rather than a guess, and it sharpens the benchmark match.",
            "title": "Role"
          },
          "sequence": {
            "anyOf": [
              {
                "maxLength": 10000,
                "minLength": 1,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "One protein chain, as amino-acid residues.",
            "title": "Sequence"
          },
          "smiles": {
            "anyOf": [
              {
                "maxLength": 10000,
                "minLength": 1,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "A ligand, as SMILES.",
            "title": "Smiles"
          }
        },
        "title": "PublicRecommendationInput",
        "type": "object"
      },
      "PublicRecommendedTool": {
        "description": "One tool, in recommended order. The ORDER is the recommendation.",
        "properties": {
          "reason": {
            "description": "Why this tool, in prose.",
            "title": "Reason",
            "type": "string"
          },
          "toolId": {
            "description": "The tool to run, as named by the tool catalogue.",
            "title": "Toolid",
            "type": "string"
          }
        },
        "required": [
          "toolId",
          "reason"
        ],
        "title": "PublicRecommendedTool",
        "type": "object"
      },
      "PublicToolRecommendation": {
        "description": "A ranked recommendation, or a reasoned refusal to make one.\n\n`abstain` is a normal outcome, not an error: it means no tool could be defended\nfor this request, and `summary` says why. Callers should handle it as an answer.",
        "properties": {
          "abstain": {
            "description": "`false` — `tools` is ranked, best first, and the first entry is what to run. `true` — no tool could be justified, `tools` is empty, and `summary` says what was missing. An abstention is an answer, not an error.",
            "title": "Abstain",
            "type": "boolean"
          },
          "summary": {
            "description": "A short explanation of the ranking, for a person to read. It also carries the caveats: whether the benchmark had measured your target, whether anything measured this kind of task at all, and what the service could not establish.",
            "title": "Summary",
            "type": "string"
          },
          "tools": {
            "description": "Ranked best-first, and only the tools worth running — usually two to four, not the whole catalogue. Empty when `abstain` is true.",
            "items": {
              "$ref": "#/components/schemas/PublicRecommendedTool"
            },
            "title": "Tools",
            "type": "array"
          }
        },
        "required": [
          "abstain",
          "summary",
          "tools"
        ],
        "title": "PublicToolRecommendation",
        "type": "object"
      },
      "PipelineToolInput": {
        "description": "Existing Jobs molecule input. Adapter support varies by tool. Use settingsKey to identify the declared public input field; do not also supply that field in settings.",
        "discriminator": {
          "propertyName": "kind"
        },
        "oneOf": [
          {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "kind",
              "sequence"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "rawSequence"
                ]
              },
              "sequence": {
                "type": "string",
                "minLength": 1
              },
              "settingsKey": {
                "type": "string",
                "nullable": true
              },
              "name": {
                "type": "string",
                "nullable": true
              }
            }
          },
          {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "kind",
              "smiles"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "smiles"
                ]
              },
              "smiles": {
                "type": "string",
                "minLength": 1
              },
              "settingsKey": {
                "type": "string",
                "nullable": true
              },
              "name": {
                "type": "string",
                "nullable": true
              }
            }
          },
          {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "kind",
              "complexId"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "pickerComplex"
                ]
              },
              "complexId": {
                "type": "string",
                "minLength": 1
              },
              "fileId": {
                "type": "string",
                "nullable": true
              },
              "inputChainId": {
                "type": "string",
                "nullable": true
              },
              "settingsKey": {
                "type": "string",
                "nullable": true
              },
              "groupId": {
                "type": "string",
                "nullable": true
              }
            }
          },
          {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "kind",
              "groupId"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "groupRef"
                ]
              },
              "groupId": {
                "type": "string",
                "minLength": 1
              }
            }
          }
        ]
      },
      "PipelineToolSubmissionResponse": {
        "type": "object",
        "required": [
          "success",
          "startedVia",
          "results"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "description": "True only when every expected run handoff is accepted."
          },
          "startedVia": {
            "type": "string",
            "enum": [
              "pipeline"
            ]
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "Jobs outcome with Id, JobName, JobStatus, handoff, ok and error. Keep Id for pipeline APIs.",
              "properties": {
                "Id": {
                  "type": "string",
                  "nullable": true
                },
                "JobName": {
                  "type": "string"
                },
                "JobStatus": {
                  "type": "string",
                  "nullable": true
                },
                "handoff": {
                  "type": "string",
                  "enum": [
                    "accepted",
                    "unconfirmed"
                  ]
                },
                "ok": {
                  "type": "boolean"
                },
                "error": {
                  "type": "string",
                  "nullable": true
                }
              },
              "additionalProperties": true
            }
          }
        }
      },
      "JobSubmission": {
        "type": "object",
        "required": [
          "jobName",
          "type",
          "settings"
        ],
        "properties": {
          "submitId": {
            "type": "string",
            "description": "Pipeline tools require a unique string per intent; reuse it with the unchanged request for retries."
          },
          "inputs": {
            "type": "array",
            "description": "Named molecule inputs accepted by the tool adapter. Supply settingsKey for multi-input tools.",
            "items": {
              "$ref": "#/components/schemas/PipelineToolInput"
            }
          },
          "jobName": {
            "type": "string",
            "description": "Name for the job, unique within your account. Characters outside [A-Za-z0-9_.- ] are stripped and whitespace becomes underscores, so a name is sanitised rather than rejected.",
            "minLength": 1,
            "example": "my-protein-analysis"
          },
          "type": {
            "type": "string",
            "description": "Tool to run. The available tools are account-scoped — fetch the live list from `GET /tools` rather than assuming a name.",
            "example": "alphafold"
          },
          "settings": {
            "type": "object",
            "additionalProperties": true,
            "description": "Tool-specific settings. The accepted fields differ per tool and per account, so they are not enumerated here: fetch the JSON Schema for the tool you are submitting from `GET /tools/{name}/schema` and validate against that. `POST /validate-job` checks a payload for free.",
            "example": {
              "sequence": "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQ"
            }
          },
          "version": {
            "type": "string",
            "description": "Optional custom-tool version returned by the build endpoint, such as `v1`. Omit it to run the published version."
          },
          "modelId": {
            "type": "string",
            "description": "Inference with a finetuned model: the model's `id` from `GET /finetuned-models`. Read only when `type` is an inference tool, and `type` must be that model's `inferenceType`. Preferred over `modelName`, because names are not unique: when both are sent, `modelId` is used. The model must be one you can access and `Available`; otherwise the submit is refused with a plain-text 400. That is a model you trained, or a teammate's that is shared with your organization (from `organizationModels`).",
            "example": "3f6c2a9e-8d41-4b7a-9c55-0e2d7f1a6b90"
          },
          "modelName": {
            "type": "string",
            "description": "Inference with a finetuned model, by its `name` from `GET /finetuned-models`, when you do not have its `id`. One of `modelId` or `modelName` is required when `type` is an inference tool — except `progen2-inference`, `flowr` and `helm-gpt`, which run their base model when neither is sent.",
            "example": "my-solubility-model"
          },
          "projectTag": {
            "$ref": "#/components/schemas/ProjectTag"
          }
        }
      },
      "BatchSubmission": {
        "type": "object",
        "required": [
          "batchName",
          "type",
          "settings"
        ],
        "properties": {
          "submitId": {
            "type": "string",
            "description": "Pipeline tools require a unique string per intent; reuse it with the unchanged request for retries."
          },
          "inputs": {
            "type": "array",
            "description": "One input list per settings row, in the same order. Accepted kinds depend on the tool adapter.",
            "items": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/PipelineToolInput"
              }
            }
          },
          "batchName": {
            "type": "string",
            "description": "Name for the batch, unique within your account. A batch name is held by a lock for 15 minutes after submission, so reusing one within that window returns 409. A batch name that is empty after normalization is rejected with 400.",
            "minLength": 1,
            "example": "my-batch-analysis"
          },
          "type": {
            "type": "string",
            "description": "Tool to run for every job in the batch.",
            "example": "alphafold"
          },
          "settings": {
            "type": "array",
            "description": "One settings object per job — the array form is what distinguishes this endpoint from `/submit-job`. See `JobSubmission.settings` for where the per-tool shape comes from.",
            "minItems": 1,
            "maxItems": 30000,
            "items": {
              "$ref": "#/components/schemas/JobSubmission/properties/settings"
            }
          },
          "version": {
            "$ref": "#/components/schemas/JobSubmission/properties/version"
          },
          "modelId": {
            "$ref": "#/components/schemas/JobSubmission/properties/modelId"
          },
          "modelName": {
            "$ref": "#/components/schemas/JobSubmission/properties/modelName"
          },
          "jobNames": {
            "type": "array",
            "description": "Optional names, the same length as `settings`. Omit to have jobs auto-named. If any name collides with an existing job, every name is rewritten as `{batchName}-{name}`.\nNo stored name may equal the batch's own name. The batch is itself a job under that name, so the two would collide and the batch could never finish; such a submission is rejected with 400.",
            "minItems": 1,
            "maxItems": 30000,
            "items": {
              "$ref": "#/components/schemas/JobSubmission/properties/jobName"
            }
          },
          "projectTag": {
            "$ref": "#/components/schemas/ProjectTag"
          },
          "repoOverrides": {
            "type": "object",
            "description": "Tamarind staff on the staging deployment only. Pins the `tamarind-utils` git ref every job in the batch clones at start. Every job must route to T2. Anywhere else the whole batch is refused with 403 and no jobs are created.",
            "maxProperties": 4,
            "additionalProperties": {
              "type": "string",
              "maxLength": 256
            },
            "example": {
              "tamarind-utils": "my-branch"
            }
          }
        }
      },
      "ProjectTag": {
        "type": "string",
        "description": "An organization project to tag the job(s) with — its project id, or its name. Applies to the whole batch on `/submit-batch`, and to every job a pipeline creates on `/submit-pipeline`.\nUsually optional. It is REQUIRED when your organization has turned on \"require a project tag\": those accounts get 400 on any submission that does not resolve to an ORGANIZATION project (a personal project does not satisfy the policy). That 400 body carries `availableProjects` — the projects you may use — so you can retry without looking them up elsewhere.",
        "example": "8f1c2b64-9a3e-4c77-b0d1-4a2f6e5d8c90"
      },
      "Project": {
        "type": "object",
        "properties": {
          "ProjectId": {
            "type": "string",
            "description": "The id to send as `projectTag`. Always accepted. `ProjectName` is accepted TOO, but only for `org` and `personal` scopes — a `shared` project is resolvable by id ONLY, because shared projects live in different creators' partitions where name uniqueness is not enforced, so a name could route the submission to the wrong project. Send the id and it always works."
          },
          "ProjectName": {
            "type": "string"
          },
          "Organization": {
            "type": "string",
            "description": "The scope that owns it — your organization's slug, or `PERSONAL#<email>`."
          },
          "Description": {
            "type": "string"
          },
          "Color": {
            "type": "string"
          },
          "CreatedBy": {
            "type": "string",
            "format": "email"
          },
          "Created": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601. The field is `Created`, not `CreatedAt`."
          },
          "Updated": {
            "type": "string",
            "format": "date-time"
          },
          "scope": {
            "type": "string",
            "enum": [
              "org",
              "personal",
              "shared"
            ],
            "description": "Which query found it, and ONLY `org` satisfies an organization's required-project policy — `personal` is your own, `shared` is someone else's private project you were invited to.\nRead it together with `isInOrg`. When `isInOrg` is false you have no organization, so your personal partition IS your default scope and every project comes back `org`; there is also no required-project policy to satisfy. Branch on `isInOrg` first, not on `scope` alone."
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Error message"
          },
          "detail": {
            "type": "string",
            "description": "Error message (V2 alias — same text as `error`, for FastAPI DomainException compatibility)"
          },
          "code": {
            "type": "string",
            "description": "Error code"
          },
          "endpoint": {
            "type": "string",
            "description": "Present on a `use_finetune_endpoint` refusal: the route to resend the submission to (`/finetune` from `/submit-job`, `/finetune-batch` from `/submit-batch`)."
          },
          "quota": {
            "$ref": "#/components/schemas/MonthlyQuota"
          },
          "details": {
            "type": "object",
            "description": "Additional error details"
          },
          "provisionApiKey": {
            "type": "string",
            "format": "uri",
            "description": "Present on a missing/invalid API-key rejection: the endpoint that mints a working key with no authentication, no signup form and no human. POST it with an empty body. It returns a real free account — every tool and the full monthly allowance, nothing held back — which expires unless a human claims it with one sign-in, keeping the same key. Listed first in the body because it is the only recovery an agent can complete on its own."
          },
          "agentAuth": {
            "type": "string",
            "format": "uri",
            "description": "Present on a missing/invalid API-key rejection: the agent-authentication document (`/auth.md`) describing the provisioning call, the trial credential's limits and the claim flow."
          },
          "getApiKey": {
            "type": "string",
            "format": "uri",
            "description": "Present on a missing/invalid API-key rejection: the page where a key is created, on the host that served this response (a tenant deployment names its own host, because a key from the shared app does not work against a tenant pod). Agents arrive by calling and failing rather than by browsing, so the way out ships with the rejection."
          },
          "agentGuide": {
            "type": "string",
            "format": "uri",
            "description": "Present on a missing/invalid API-key rejection: the LLM-readable API guide."
          },
          "toolCatalog": {
            "type": "string",
            "format": "uri",
            "description": "Present on a missing/invalid API-key rejection: the keyless tool catalog, so a correct payload can be built before a key exists."
          },
          "hint": {
            "type": "string",
            "description": "Plain-language next step, present on the rejections that have one. On a missing/invalid API-key rejection it gives the agent path (POST `provisionApiKey`, which needs no authentication) and the human path (create a key in the web app) separately, because the right answer differs by caller; on a required-project rejection it says to retry with `projectTag` set to one of `availableProjects`."
          },
          "availableProjects": {
            "type": "array",
            "description": "OPTIONAL, on a required-project rejection (see `ProjectTag`): organization projects this caller may tag a submission with, sent WITH the refusal so a retry needs no second call. At most 25.\nDo not depend on it. The lookup is bounded and fails open, so the field is ABSENT whenever it found nothing — and its absence says nothing about whether projects exist. `hint` is always present on this rejection and points at `GET /projects/list`, which is the complete answer.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Pass this as `projectTag`."
                },
                "name": {
                  "type": "string",
                  "description": "The project's display name. Also accepted as `projectTag`."
                }
              }
            }
          },
          "jobCount": {
            "type": "integer",
            "description": "OPTIONAL, on the `DELETE /projects/delete` refusal only: how many jobs are still filed under the project, and so how many the delete would take out of it. It is there to be quoted back to a user before you retry the call with `moveToProjectId` or `confirmJobRemoval`, which is the only thing the refusal is waiting for."
          }
        }
      },
      "PublicToolInfo": {
        "type": "object",
        "description": "One tool in the keyless catalogue (`/tools-catalog`). Note the field names differ from `ToolInfo` on purpose: the wire value is `type` here, matching the key you actually send to `/submit-job`, while the human label is `displayName`. (`ToolInfo` calls the same wire value `name`.) A display name is never a valid `type`.",
        "properties": {
          "type": {
            "type": "string",
            "description": "The exact, case-sensitive value to send as `type` when submitting.",
            "example": "alphafold"
          },
          "finetune": {
            "type": "boolean",
            "description": "True for a finetuning tool. Submit it with `POST /finetune`, sending this value as `model` rather than `type`."
          },
          "displayName": {
            "type": "string",
            "description": "Human label for the tool. NOT a valid `type` value.",
            "example": "AlphaFold"
          },
          "description": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "description": "Intent tags, usable as the `tag` query parameter.",
            "items": {
              "type": "string"
            },
            "example": [
              "structure-prediction"
            ]
          },
          "taskSetting": {
            "type": "string",
            "description": "The setting whose value the `tasks` predicates below refer to. Present only for multi-task tools. Do NOT assume it is called `task`: it is `metricType` on `protein-metrics`, `binderType` on `boltzgen` and `inputFormat` on `boltz`, `chai` and `esmfold2`, among 55 tools that name it something else. Sending the wrong key produces no error — unrecognised settings keys are flagged, not rejected — just the wrong set of required fields.",
            "example": "metricType"
          },
          "requiredSettings": {
            "type": "array",
            "description": "The tool's REQUIRED settings only — enough to build a valid payload without a key. Optional parameters, defaults and descriptions need `/tools/{name}/schema`. Send these names verbatim: an unrecognised key is flagged, not rejected, so a typo shows up only as the real field reading as missing.\n\nRequiredness is usually CONDITIONAL, so read `tasks` and `conditionals` before treating this as a checklist: most multi-task tools list alternative branches, not fields that are all needed at once. `proto`, for example, requires `task` plus exactly ONE of the remaining entries, and `rfdiffusion3` lists `jsonFile` and `jsonConfig`, which are mutually exclusive.",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Send this exact key inside `settings`."
                },
                "required": {
                  "type": "boolean",
                  "description": "Present and `false` on entries you need NOT send. They appear in this list only because another entry's `tasks` or `conditionals` names them, and a predicate whose controller is missing from the document cannot be acted on. Read their `default` — it is what silently selects a branch: `pysca.useMSA` defaults to true, so a payload sending only `fastaFile` takes the other branch and is rejected for a missing `sequence`. Omitted entirely (not set to `true`) on genuinely required entries."
                },
                "type": {
                  "type": "string",
                  "description": "The field's input type, e.g. `string`, `number`, `file`."
                },
                "list": {
                  "type": "boolean",
                  "description": "When true, send an ARRAY of `type`, not a single value. Omitted when false."
                },
                "lowerBound": {
                  "type": "number",
                  "description": "Enforced minimum. A smaller value is rejected."
                },
                "upperBound": {
                  "type": "number",
                  "description": "Enforced maximum. A larger value is rejected."
                },
                "minItems": {
                  "type": "integer",
                  "description": "Enforced minimum array length for a `list: true` field. A shorter array is rejected with a 400. Published alongside the constraint."
                },
                "maxItems": {
                  "type": "integer",
                  "description": "Enforced maximum array length for a `list: true` field. A longer array is rejected with a 400. Published alongside the constraint."
                },
                "maxLength": {
                  "type": "integer",
                  "description": "Maximum sequence length in residues (whitespace is not counted). Exceeding it is a 400. Published for any `sequence`-type field (list-valued fields cap every entry). Limits range from 14 residues to 20,000, so check before sending a long chain."
                },
                "minLength": {
                  "type": "integer",
                  "description": "Minimum sequence length in residues (whitespace is not counted). A shorter entry is rejected with a 400. Published for any `sequence`-type field (list-valued fields floor every entry)."
                },
                "maxSingleChainLength": {
                  "type": "integer",
                  "description": "Tighter cap for a single-chain value (no `:`), in residues. Exceeding it is a 400. A complex is held only to `maxLength`."
                },
                "maxLengthNote": {
                  "type": "string",
                  "description": "Why the cap exists, where the tool explains it — usually \"this tool folds only the antibody variable domain (Fv)\". Read it before truncating to fit."
                },
                "sep": {
                  "type": "string",
                  "description": "The literal delimiter for a residue-selection field, published when the field ships no `example`. `\" \"` means space-separated (`\"1 2 3\"`); `\",\"` means comma-separated ranges (`\"1-5,7,9-11\"`). This matters more than it looks: the wrong form is NOT rejected, it is read as different residues, so the job silently runs on the wrong positions."
                },
                "contigsFormat": {
                  "type": "string",
                  "description": "Names the form a residue-selection field expects (e.g. `ranges`). Published alongside `sep` where the tool defines it."
                },
                "unknownResidue": {
                  "type": "string",
                  "enum": [
                    "X-stripped"
                  ],
                  "description": "Present on protein sequence fields only. `X` is NOT covered by `alphabet` and is NOT rejected: the validator accepts it and normalization REMOVES it before the job runs, which shifts every residue index after it. Absent on nucleotide fields, where `X` really is a 400."
                },
                "alphabet": {
                  "type": "string",
                  "description": "The accepted residue characters. An out-of-alphabet character is rejected with a 400 naming this same set — except `X` on a field carrying `unknownResidue`, which is accepted and stripped. Do NOT assume `type: \"sequence\"` means protein: `disco.dnaSequence` accepts `ATGCN` and `rna-fm.sequence` accepts `ACGU`, so a protein chain sent to either is refused. Whitespace is tolerated and may appear in the published set."
                },
                "defaultByTask": {
                  "description": "A per-task LOOKUP, not a value to send: read your branch out of it. Present instead of `default` when the tool's default varies by the task selector — publishing `{\"antibody\": [...], \"nanobody\": [...]}` as `default` would invite a consumer to submit the map where an array is expected. A map covering every selector option is autofilled whatever branch you pick, so those fields are not listed as required at all."
                },
                "singleChain": {
                  "type": "boolean",
                  "description": "Present and true when a colon-separated complex is REJECTED on this field. `:` is how every other sequence field in this API expresses a multi-chain input, so this is not inferable from `type: \"sequence\"`."
                },
                "minChains": {
                  "type": "integer",
                  "description": "Minimum colon-separated chains. Enforced before ordinary field validation, so a payload below it fails early."
                },
                "maxChains": {
                  "type": "integer",
                  "description": "Maximum colon-separated chains."
                },
                "subfields": {
                  "type": "array",
                  "description": "Present when each item is an OBJECT rather than a scalar — `type` and `list` alone cannot express that, and submitting a bare string is rejected. Each entry gives an item key, and its `options` where the key is an enum. Read those per tool instead of assuming a shared set: `chai` accepts a `glycan` molecule type, `boltz` does not, and `rf3` takes only `protein` and `ligand`.",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      },
                      "options": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "list": {
                        "type": "boolean"
                      },
                      "required": {
                        "type": "boolean"
                      }
                    }
                  }
                },
                "example": {
                  "description": "One filled-in value, published only for settings that carry `subfields` — where the nesting is the hard part. Not published for ordinary scalar settings, whose `type` already says what to send."
                },
                "default": {
                  "description": "The value that applies if you omit this setting. Present only where it is load-bearing — a task selector, or a field another entry's `conditionals` depend on. Omitting `rfdiffusion3.jsonInputType`, for instance, silently applies `file`, which is what decides whether `jsonFile` or `jsonConfig` becomes required."
                },
                "options": {
                  "type": "array",
                  "description": "When present, the value must be one of these.",
                  "items": {
                    "type": "string"
                  }
                },
                "extension": {
                  "type": "array",
                  "description": "For file fields, the EFFECTIVE accepted extensions — what submission actually takes, not the registry's raw list. Any list containing `pdb` is widened with `cif`, because the validator accepts a CIF for a PDB field and converts it before dispatch. Fields that opt out of that conversion keep their raw list.",
                  "items": {
                    "type": "string"
                  }
                },
                "tasks": {
                  "type": "array",
                  "description": "When present, this setting is required ONLY if the tool's task selector holds one of these values. The selector is named by the entry's `taskSetting` — it is not always `task`. Absent means unconditionally required.",
                  "items": {
                    "type": "string"
                  }
                },
                "conditionals": {
                  "type": "array",
                  "description": "When present, this setting is required ONLY while another setting holds a given value — the mechanism behind mutually exclusive inputs.",
                  "items": {
                    "type": "object",
                    "properties": {
                      "otherSettingName": {
                        "type": "string",
                        "description": "The setting this one depends on."
                      },
                      "otherSettingValue": {
                        "description": "The value that makes this setting required."
                      },
                      "checkType": {
                        "type": "string",
                        "description": "How the values are compared, e.g. `equals`."
                      }
                    }
                  }
                }
              },
              "required": [
                "name"
              ]
            }
          }
        },
        "required": [
          "type"
        ]
      },
      "ToolInfo": {
        "type": "object",
        "description": "One tool in the catalogue. `settings` describes its parameters; fetch `GET /tools/{name}/schema` for the same information as JSON Schema.\n\n`taskType` is the tool's pipeline task category (`structure-prediction`, `inverse-folding`, `score`, …) — the fact pipelines chain on. Two tools connect when the molecule type one produces matches what the next consumes (a `pdb` output may also feed a `sequence` input, since sequences are read from structures).",
        "properties": {
          "name": {
            "type": "string",
            "description": "The value to send as `type` when submitting.",
            "example": "alphafold"
          },
          "pipelineTool": {
            "type": "boolean",
            "description": "True for a pipeline-backed tool. Supply submitId, retain it for retries, and read PipelineToolSubmissionResponse instead of an ordinary job response."
          },
          "finetune": {
            "type": "boolean",
            "description": "True for a finetuning tool, which is submitted with `POST /finetune` (name it in `model`) rather than `/submit-job`."
          },
          "displayName": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "github": {
            "type": "string"
          },
          "paper": {
            "type": "string"
          },
          "settings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "required": {
                  "type": "boolean"
                },
                "type": {
                  "type": "string",
                  "description": "Present only for a subset of parameter kinds — read it defensively, and prefer the JSON Schema from `/tools/{name}/schema`."
                },
                "description": {
                  "type": "string"
                },
                "options": {
                  "type": "array",
                  "items": {}
                },
                "default": {},
                "extension": {
                  "type": "array",
                  "description": "For a file parameter, the file formats it accepts — the only published statement of what this tool's parser can read.",
                  "items": {
                    "type": "string"
                  },
                  "example": [
                    "pdb",
                    "cif"
                  ]
                },
                "list": {
                  "type": "boolean",
                  "description": "True when this parameter takes an ARRAY of the stated `type` rather than a single value."
                }
              }
            }
          },
          "outputTypes": {
            "type": "array",
            "description": "What the tool declares it produces — molecular (`pdb`, `sequence`, `sdf`, `smiles`) alongside file and score types (`csv`, `score`, `cif`, …). A few tools use compound values such as `pdb-list`, so match by containment rather than equality.\n\nThis describes the ARTIFACTS a run leaves behind. To chain stages, match on `taskType` and the molecule types: a scoring stage passes its input type through even though its `outputTypes` says `score`. Absent means the tool declares nothing, which is unknown rather than \"produces nothing\"; and a type here is not proof the tool generated it, since a scoring tool can echo its input.",
            "items": {
              "type": "string"
            },
            "example": [
              "pdb"
            ]
          },
          "taskType": {
            "type": "string",
            "description": "The tool's pipeline task category (`structure-prediction`, `inverse-folding`, `score`, …) — the fact pipelines chain on. Absent for a tool that declares none.",
            "example": "structure-prediction"
          },
          "filterMetrics": {
            "type": "array",
            "description": "Metric names accepted in a pipeline stage's filter settings. Deliberately narrower than `outputs.columns` — `/submit-pipeline` rejects a filter on a column that is not filterable. Absent for tools whose filters are not validated.",
            "items": {
              "type": "string"
            }
          },
          "outputs": {
            "type": "object",
            "description": "The tool's result table, when it declares one. (The tool's `taskType` is a TOP-LEVEL field, not nested here.)",
            "properties": {
              "produces": {
                "type": "array",
                "description": "Molecular representations the output carries — as a column of the result table OR as a file written alongside it — INCLUDING any carried over from the input, so a scoring tool can list one here.",
                "items": {
                  "type": "string"
                }
              },
              "mainCSV": {
                "type": "string",
                "description": "Filename of the primary results CSV."
              },
              "byTask": {
                "type": "object",
                "description": "For a tool whose output depends on the task it runs: the per-task contract, keyed by task value, and authoritative for the task you set. Each entry is COMPLETE — a task that does not redeclare `mainCSV` or `taskType` inherits the tool's top-level value (the top-level `taskType` field and this block's `mainCSV`).\n\nThe top-level `taskType` and this block's `mainCSV`/`produces` summarize ACROSS tasks — `produces` is the union over every task's table, and `taskType`/`mainCSV` are the tool's top-level declaration, which is not guaranteed to be the task selector's default. Do not read them as describing the run you are about to submit.",
                "additionalProperties": {
                  "type": "object",
                  "properties": {
                    "taskType": {
                      "type": "string"
                    },
                    "mainCSV": {
                      "type": "string"
                    },
                    "generates": {
                      "type": "array",
                      "description": "What this task creates FRESH — distinct from the sibling `produces`, which is what the result table contains. Empty for a scoring task, whose table may still echo its input.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "byTaskNote": {
                "type": "string",
                "description": "Present alongside `byTask`: restates in prose how to read the per-task contract against the across-task scalars."
              },
              "columns": {
                "type": "array",
                "description": "Columns of the main CSV. To FILTER on one in a pipeline, check `filterMetrics` — not every column is filterable.",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "description": "Column name as it appears in the CSV."
                    },
                    "type": {
                      "type": "string",
                      "description": "Column type: pdb, sequence, number, string, …"
                    },
                    "displayName": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    },
                    "units": {
                      "type": "string"
                    },
                    "scoringPropertyName": {
                      "type": "string",
                      "description": "Name this metric is stored under when results are ingested, for tools that expose the column as a scoring property."
                    },
                    "recommendedRange": {
                      "type": "array",
                      "description": "Advisory [min, max] for a good value; either end may be an empty string when unbounded on that side.",
                      "items": {}
                    },
                    "lowIsGood": {
                      "type": "boolean",
                      "description": "Present and true when a LOWER value is better (RMSD, PAE, energy) — the direction to rank in."
                    },
                    "tasks": {
                      "type": "array",
                      "description": "Present only on a task-gated tool: the tasks whose results include this column. An UNTAGGED column is simply not task-gated in the tool's declaration, which does not promise every task fills it.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "MonthlyQuota": {
        "type": "object",
        "description": "Present alongside `code: monthly_limit_exceeded` on `/submit-job`, `/submit-batch` and `/validate-job` — the counts behind the verdict, so you do not have to parse them out of the message. One shape across all three: the same refusal reads the same way whether you hit it at pre-flight or at submit.",
        "properties": {
          "used": {
            "type": "integer",
            "description": "Jobs already counted against this calendar month."
          },
          "limit": {
            "type": "integer",
            "description": "The account's monthly job allowance."
          },
          "remaining": {
            "type": "integer",
            "description": "`limit - used`, floored at 0."
          },
          "requested": {
            "type": "integer",
            "description": "How many jobs THIS payload would create — one for an ordinary submission, the batch length for an array, and the design count for a `protein-design` fanout. Absent when the caller was not asking about a specific payload."
          }
        }
      },
      "ValidationResult": {
        "type": "object",
        "required": [
          "valid"
        ],
        "properties": {
          "valid": {
            "type": "boolean"
          },
          "normalized": {
            "type": "object",
            "additionalProperties": true,
            "description": "Present when valid — the settings to submit, with defaults filled in."
          },
          "job_name": {
            "type": "string",
            "description": "The name the job would actually be STORED under, present whenever you sent a `jobName`. Submission strips everything outside `[A-Za-z0-9_\\s.-]` and turns whitespace into `_`, so \"PD-L1 binder #3\" is stored as \"PD-L1_binder_3\". Key every later lookup (`/jobs`, `/result`, `/job-logs`) on THIS value — the original name answers \"not found\"."
          },
          "job_name_changed": {
            "type": "boolean",
            "description": "Present and true only when `job_name` differs from the `jobName` you sent."
          },
          "error": {
            "type": "string",
            "description": "Present when invalid — the first problem found."
          },
          "code": {
            "type": "string",
            "description": "Present when the refusal is a class you can act on rather than a payload problem. `monthly_limit_exceeded` is the same slug `/submit-job` returns for the same refusal, and it is checked LAST — after the tool resolves, the name is confirmed free, the settings validate and the free-tier caps are cleared — so every check this endpoint makes has passed and only the ceiling is in the way. Not a guarantee that submit will accept: org compute budgets, queue depth and per-run entitlements are weighed at submission and cannot be evaluated without reserving."
          },
          "quota": {
            "$ref": "#/components/schemas/MonthlyQuota"
          },
          "batchingUnsupported": {
            "type": "boolean",
            "description": "Present and true when a CUSTOM tool's design count exceeds its `designsPerBatch`. Both submit endpoints refuse this outright — the fan-out belongs to the splitter behind `/api/v2/jobs` — so it is a routing problem, not a payload to fix. Submit from the app or to `/api/v2/jobs`, or lower the count."
          },
          "designCountTooLarge": {
            "type": "boolean",
            "description": "Present and true when a custom tool's design count exceeds that field's cap (`upperBound`). Lower the count; the same refusal comes back from submit."
          },
          "count": {
            "type": "integer",
            "description": "Array mode only — how many rows were checked."
          },
          "valid_count": {
            "type": "integer",
            "description": "Array mode only — how many of those rows passed."
          },
          "results": {
            "type": "array",
            "description": "Array mode only — one verdict per row, in input order, each carrying the same fields a single-payload verdict does plus its `index`. Absent when the request is refused as a whole (out of quota, over the row cap, a bad `jobNames`, verdicts too large to return), which replaces the body rather than appearing per row.\n\nPRESENCE of this field, not `valid`, is what tells the two apart: both answer `valid: false`. With `results` the diagnostics are per row and there is no top-level `error`; without it the top-level `error` says why the request was refused.",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/ValidationResult"
                },
                {
                  "type": "object",
                  "properties": {
                    "index": {
                      "type": "integer",
                      "description": "This row's position in the `settings` array you sent."
                    }
                  }
                }
              ]
            }
          },
          "missing_fields": {
            "type": "array",
            "description": "Best-effort list of required inputs still missing. May be empty even when `valid` is false, because validation stops at the first error.",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "displayName": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "example": {}
              }
            }
          }
        }
      },
      "PipelineStage": {
        "type": "object",
        "required": [
          "task",
          "toolSettings"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Ignored on submit. The server assigns each stage a 1-based index and uses that in error messages, overwriting anything sent here."
          },
          "task": {
            "type": "string",
            "description": "What this stage does, e.g. \"Structure Prediction\"."
          },
          "tools": {
            "type": "array",
            "description": "Optional and derived — submission overwrites it with the keys of `toolSettings` (submit-pipeline.js), so an empty array is accepted. It only widens the set checked against your account's tool access, so naming a tool here without settings for it grants nothing.",
            "items": {
              "type": "string"
            }
          },
          "toolSettings": {
            "type": "object",
            "additionalProperties": true,
            "minProperties": 1,
            "description": "Settings per tool, keyed by tool name — this is what determines which tools the stage runs, so it may not be empty. Each value follows that tool's schema from `GET /tools/{name}/schema`."
          },
          "scoringTools": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "scoringToolSettings": {
            "type": "object",
            "additionalProperties": true
          },
          "filterSettings": {
            "type": "object",
            "additionalProperties": true,
            "description": "Metric filters applied to this stage's outputs. Metric names are case-sensitive and tool-specific; an unknown one is rejected with the valid options listed."
          }
        }
      },
      "FinetunedModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Send as `modelId` on `POST /submit-job` to run inference with this model, or as `settings.parentModelId` on `POST /finetune` to train its next version."
          },
          "name": {
            "type": "string",
            "description": "The model's job name. Accepted as `modelName` when submitting; prefer `id`."
          },
          "displayName": {
            "type": "string",
            "description": "The name shown in the app. Falls back to `name`."
          },
          "type": {
            "type": "string",
            "description": "The finetuning tool that trained the model."
          },
          "inferenceType": {
            "type": [
              "string",
              "null"
            ],
            "description": "The tool to submit as `type` on `POST /submit-job` to run this model."
          },
          "baseModel": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "Only an `Available` model can run inference."
          },
          "created": {
            "type": "string"
          },
          "version": {
            "type": [
              "integer",
              "null"
            ]
          },
          "parentModelId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The `id` of the model this one was trained from, if any."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "published": {
            "type": "boolean"
          },
          "sharedWithOrg": {
            "type": "boolean"
          },
          "isDefault": {
            "type": "boolean"
          },
          "starred": {
            "type": "boolean"
          },
          "owner": {
            "type": "string",
            "description": "Present only when the model belongs to another member of your organization."
          }
        }
      },
      "DiagnosticCode": {
        "type": "string",
        "enum": [
          "parse-error",
          "shape-violation",
          "unsupported-field",
          "invalid-type",
          "missing-field",
          "invalid-option",
          "invalid-filter-shape",
          "structural-limit",
          "cycle",
          "dangling-ref",
          "unsupported-schema-version",
          "tool-unknown",
          "setting-invalid",
          "param-out-of-range",
          "required-field-unset",
          "input-unbound",
          "input-missing-reference",
          "chain-incompatible",
          "chain-unsatisfied",
          "molecule-class-incompatible",
          "binding-invalid",
          "budget-exceeded",
          "tool-not-licensed",
          "runtime-unresolved",
          "unknown"
        ],
        "title": "DiagnosticCode",
        "description": "The PUBLIC validation-diagnostic vocabulary — the stable value set a caller may switch on.\n\nThis is a CURATED contract, not a passthrough of the internal code set: the mapper\n(`_map/pipelines._diagnostic`) translates each internal code to one of these, and an internal\ncode with no public mapping becomes `unknown` (never a raw internal string). So a new INTERNAL\ndiagnostic code cannot silently enter the public contract — adding a public code is a deliberate,\nv1-frozen change. Keep in sync with the mapper's translation table."
      },
      "Flow": {
        "type": "string",
        "enum": [
          "molecule",
          "file"
        ],
        "title": "Flow"
      },
      "LegacyJobNamesPage": {
        "properties": {
          "jobs": {
            "items": {
              "$ref": "#/components/schemas/LegacyJobSummary"
            },
            "type": "array",
            "title": "Jobs"
          },
          "notFound": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Notfound"
          },
          "statuses": {
            "additionalProperties": {
              "type": "integer"
            },
            "type": "object",
            "title": "Statuses"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "jobs",
          "notFound",
          "statuses"
        ],
        "title": "LegacyJobNamesPage",
        "description": "The batched exact-name lookup: one row per name that resolved, and the names that did not.\n\nA miss is reported per name rather than as the whole-request 400 a single-name lookup returns,\nbecause \"which of these already exist\" is the question this shape exists to answer -- a caller\nresuming an interrupted campaign needs the misses as data, not as an error.\n\nExactly one row per resolved name, the newest. Under `organization=true` a name can match once\nper visible member; this shape answers the most recent, and the single-name read is where every\nmatch is available. That is also what bounds the response: rows cannot outnumber the names\nasked for, and every `JobName` returned came from the request body."
      },
      "LegacyJobNamesRequest": {
        "properties": {
          "jobNames": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Jobnames"
          },
          "organization": {
            "type": "boolean",
            "title": "Organization",
            "default": false
          },
          "includeSubjobs": {
            "type": "boolean",
            "title": "Includesubjobs",
            "default": false
          },
          "jobEmail": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Jobemail"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "jobNames"
        ],
        "title": "LegacyJobNamesRequest",
        "description": "The batched exact-name lookup's request body.\n\nThe names travel in a body rather than a query string because they do not fit in one: an exact\nname may be 200 characters, and the deployment stops accepting a URL well before a useful number\nof them, with a proxy-level 414 no handler can turn into a usable error. `extra=\"forbid\"` so a\nmisspelled field is refused rather than silently ignored."
      },
      "LegacyJobPage": {
        "properties": {
          "jobs": {
            "items": {
              "$ref": "#/components/schemas/LegacyJobSummary"
            },
            "type": "array",
            "title": "Jobs"
          },
          "startKey": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Startkey"
          },
          "statuses": {
            "additionalProperties": {
              "type": "integer"
            },
            "type": "object",
            "title": "Statuses"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "jobs",
          "statuses"
        ],
        "title": "LegacyJobPage"
      },
      "LegacyJobSummary": {
        "properties": {
          "JobName": {
            "type": "string",
            "title": "Jobname"
          },
          "JobStatus": {
            "type": "string",
            "title": "Jobstatus"
          },
          "Created": {
            "type": "string",
            "title": "Created"
          },
          "Type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Type"
          },
          "Settings": {
            "$ref": "#/components/schemas/LegacySettingsValue"
          },
          "Score": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              },
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Score"
          },
          "Started": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Started"
          },
          "Completed": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Completed"
          },
          "Batch": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Batch"
          },
          "TamarindSchemaVersion": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Tamarindschemaversion"
          },
          "WeightedHours": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Weightedhours"
          },
          "User": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "User"
          },
          "batchStatus": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "Running",
                  "Aggregating",
                  "Complete",
                  "Stopped",
                  "AggregationFailed"
                ]
              },
              {
                "type": "null"
              }
            ],
            "title": "Batchstatus"
          },
          "AggregationError": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Aggregationerror"
          },
          "resultUrl": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Resulturl"
          },
          "aggregateStatus": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "InProgress",
                  "Complete",
                  "Failed"
                ]
              },
              {
                "type": "null"
              }
            ],
            "title": "Aggregatestatus"
          },
          "aggregateJobName": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Aggregatejobname"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "JobName",
          "JobStatus",
          "Created"
        ],
        "title": "LegacyJobSummary",
        "description": "Fields published by the old handler after its explicit keep-list and enrichments."
      },
      "LegacyMultiJob": {
        "additionalProperties": {
          "anyOf": [
            {
              "$ref": "#/components/schemas/LegacyJobSummary"
            },
            {
              "additionalProperties": {
                "type": "integer"
              },
              "type": "object"
            }
          ]
        },
        "type": "object",
        "title": "LegacyMultiJob",
        "description": "The retired array-object spread emitted for an organization name collision.\n\nThis odd shape is intentionally isolated to the one historical edge case instead of weakening\nthe normal page or single-job DTOs."
      },
      "LegacySettingsValue": {
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "integer"
          },
          {
            "type": "number"
          },
          {
            "type": "boolean"
          },
          {
            "items": {},
            "type": "array"
          },
          {
            "additionalProperties": true,
            "type": "object"
          },
          {
            "type": "null"
          }
        ]
      },
      "LegacySingleJob": {
        "properties": {
          "JobName": {
            "type": "string",
            "title": "Jobname"
          },
          "JobStatus": {
            "type": "string",
            "title": "Jobstatus"
          },
          "Created": {
            "type": "string",
            "title": "Created"
          },
          "Type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Type"
          },
          "Settings": {
            "$ref": "#/components/schemas/LegacySettingsValue"
          },
          "Score": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              },
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Score"
          },
          "Started": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Started"
          },
          "Completed": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Completed"
          },
          "Batch": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Batch"
          },
          "TamarindSchemaVersion": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Tamarindschemaversion"
          },
          "WeightedHours": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Weightedhours"
          },
          "User": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "User"
          },
          "batchStatus": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "Running",
                  "Aggregating",
                  "Complete",
                  "Stopped",
                  "AggregationFailed"
                ]
              },
              {
                "type": "null"
              }
            ],
            "title": "Batchstatus"
          },
          "AggregationError": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Aggregationerror"
          },
          "resultUrl": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Resulturl"
          },
          "aggregateStatus": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "InProgress",
                  "Complete",
                  "Failed"
                ]
              },
              {
                "type": "null"
              }
            ],
            "title": "Aggregatestatus"
          },
          "aggregateJobName": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Aggregatejobname"
          },
          "statuses": {
            "additionalProperties": {
              "type": "integer"
            },
            "type": "object",
            "title": "Statuses"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "JobName",
          "JobStatus",
          "Created",
          "statuses"
        ],
        "title": "LegacySingleJob"
      },
      "MoleculeChainInfo": {
        "properties": {
          "type": {
            "type": "string",
            "title": "Type"
          },
          "tags": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Tags"
          }
        },
        "type": "object",
        "required": [
          "type",
          "tags"
        ],
        "title": "MoleculeChainInfo",
        "description": "Type + role tags of one chain in a molecule's `entity`, keyed by the same\nchain id — so a reader tells a protein sequence from a SMILES, and sees\nheavy/light/lead roles, without loading the group's schema."
      },
      "MoleculeClass": {
        "type": "string",
        "enum": [
          "protein",
          "small_molecule",
          "nucleic_acid"
        ],
        "title": "MoleculeClass"
      },
      "MoleculeFileEntry": {
        "properties": {
          "fileName": {
            "type": "string",
            "title": "Filename"
          },
          "fileType": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Filetype"
          },
          "downloadUrl": {
            "type": "string",
            "title": "Downloadurl"
          },
          "createdAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Createdat"
          }
        },
        "type": "object",
        "required": [
          "fileName",
          "fileType",
          "downloadUrl",
          "createdAt"
        ],
        "title": "MoleculeFileEntry",
        "description": "One structure file on a molecule. Appears in the `files` map, whose KEY is\nthe producer (a job/tool name, or `user` for uploads)."
      },
      "MoleculeType": {
        "type": "string",
        "enum": [
          "protein",
          "antibody",
          "peptide",
          "enzyme",
          "small_molecule",
          "nucleic_acid",
          "small_molecule_binding_protein"
        ],
        "title": "MoleculeType",
        "description": "The spec's `MoleculeType` — the kind of a molecule, and of the molecules a\ngroup holds.\n\nValue-identical to the internal `Modality` (the user-picked upload modality),\nwhich is the superset enum `complexes.type` is written from. Declared\nseparately because it is a PUBLISHED contract: `Modality` is free to grow a\nvalue for an internal picker without that value silently becoming part of the\npublic API. `public_types_match_modality` pins them equal today."
      },
      "NodeRunStatus": {
        "type": "string",
        "enum": [
          "waiting",
          "queued",
          "running",
          "finished",
          "failed",
          "skipped",
          "stopped",
          "cancelled"
        ],
        "title": "NodeRunStatus"
      },
      "PipelineIR": {
        "properties": {},
        "additionalProperties": true,
        "type": "object",
        "title": "PipelineIR",
        "description": "A pipeline IR document. Full schema (typed, versioned): https://tamarind.bio/schemas/pipeline-v1.json"
      },
      "PublicBinding": {
        "anyOf": [
          {
            "$ref": "#/components/schemas/PublicMoleculeBinding"
          },
          {
            "$ref": "#/components/schemas/PublicFileBinding"
          }
        ],
        "title": "PublicBinding"
      },
      "PublicChainMappingEntry": {
        "properties": {
          "type": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PublicChainType"
              },
              {
                "type": "null"
              }
            ]
          },
          "tags": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/PublicChainTag"
                },
                "type": "array",
                "maxItems": 3
              },
              {
                "type": "null"
              }
            ],
            "title": "Tags",
            "description": "Roles for this chain. A chain is `heavy` OR `light`, never both (storage keeps one subtype); `lead` is compatible with either. Omit to inherit the schema's tag."
          },
          "csvColumn": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 500,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Csvcolumn"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "PublicChainMappingEntry",
        "description": "The spec's `ChainMappingEntry` — how ONE chain is defined for ingestion,\nkeyed by chain id.\n\nThis is what REPLACED the old fixed CSV role vocabulary\n(`heavy_chain`/`light_chain`/`sequence`/...), which could only describe\nantibody-shaped data and could not name a chain id at all (design notes §1)."
      },
      "PublicChainTag": {
        "type": "string",
        "enum": [
          "heavy",
          "light",
          "lead"
        ],
        "title": "PublicChainTag",
        "description": "The spec's `ChainTag` — the functional role of one chain."
      },
      "PublicChainType": {
        "type": "string",
        "enum": [
          "protein",
          "small_molecule"
        ],
        "title": "PublicChainType",
        "description": "The spec's `ChainType` — the molecular kind of one chain."
      },
      "PublicCommitQueued": {
        "properties": {
          "importId": {
            "type": "string",
            "title": "Importid"
          },
          "status": {
            "type": "string",
            "const": "queued",
            "title": "Status",
            "default": "queued"
          },
          "groupName": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Groupname"
          }
        },
        "type": "object",
        "required": [
          "importId",
          "groupName"
        ],
        "title": "PublicCommitQueued",
        "description": "Returned after the import is queued."
      },
      "PublicCommitRequest": {
        "properties": {
          "columnMapping": {
            "anyOf": [
              {
                "additionalProperties": {
                  "type": "string"
                },
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Columnmapping"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "PublicCommitRequest",
        "description": "The spec's `CommitRequest` — optional overrides applied at commit time."
      },
      "PublicCreateGroupRequest": {
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200,
            "minLength": 1,
            "title": "Name"
          },
          "type": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoleculeType"
              },
              {
                "type": "null"
              }
            ]
          },
          "schemaId": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 200,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Schemaid"
          },
          "orgId": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 200,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Orgid"
          },
          "tags": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "maxItems": 64,
            "title": "Tags"
          },
          "metadata": {
            "additionalProperties": true,
            "type": "object",
            "title": "Metadata"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "name"
        ],
        "title": "PublicCreateGroupRequest",
        "description": "The spec's `CreateGroupRequest`.\n\nInline group creation was dropped from upload/import, so this is now the ONLY\nway a group comes into existence on the public surface."
      },
      "PublicCreateSchemaRequest": {
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200,
            "minLength": 1,
            "title": "Name"
          },
          "fields": {
            "items": {
              "$ref": "#/components/schemas/PublicSchemaField"
            },
            "type": "array",
            "maxItems": 200,
            "minItems": 1,
            "title": "Fields"
          },
          "orgId": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 200,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Orgid"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "name",
          "fields"
        ],
        "title": "PublicCreateSchemaRequest"
      },
      "PublicCreateTemplateRequest": {
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200,
            "minLength": 1,
            "title": "Name"
          },
          "description": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "pipeline": {
            "$ref": "#/components/schemas/PipelineIR",
            "description": "The pipeline IR. Every molecule `user_input` node must name a reference group in `metadata.defaultGroup` (the molecules the template is authored against — a run binds its own group at submit); a molecule input without one is rejected 422 `input-missing-reference`. File inputs are exempt."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "name",
          "pipeline"
        ],
        "title": "PublicCreateTemplateRequest",
        "example": {
          "description": "AF2 + ProteinMPNN",
          "name": "binder-design",
          "pipeline": {
            "nodes": {
              "af2": {
                "inputs": {
                  "sequence": [
                    {
                      "node": "target"
                    }
                  ]
                },
                "kind": "tool",
                "tool": "tamarind://alphafold"
              },
              "target": {
                "flow": "molecule",
                "kind": "user_input",
                "metadata": {
                  "defaultGroup": "3f8a1c2e-5b7d-4e9f-a1b2-c3d4e5f6a7b8"
                },
                "molecule_type": "protein"
              }
            },
            "schema_version": "1.0"
          }
        }
      },
      "PublicDeleteMoleculeResponse": {
        "properties": {
          "moleculeId": {
            "type": "string",
            "title": "Moleculeid"
          },
          "deleted": {
            "type": "boolean",
            "title": "Deleted"
          }
        },
        "type": "object",
        "required": [
          "moleculeId",
          "deleted"
        ],
        "title": "PublicDeleteMoleculeResponse"
      },
      "PublicDiagnostic": {
        "properties": {
          "code": {
            "$ref": "#/components/schemas/DiagnosticCode"
          },
          "severity": {
            "$ref": "#/components/schemas/Severity"
          },
          "node": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Node"
          },
          "field": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Field"
          },
          "message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Message"
          }
        },
        "type": "object",
        "required": [
          "code",
          "severity",
          "node",
          "message"
        ],
        "title": "PublicDiagnostic"
      },
      "PublicDuplicateTemplateRequest": {
        "properties": {
          "name": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 200
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "version": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Version",
            "description": "A version handle e.g. 'v3'; absent -> default."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "PublicDuplicateTemplateRequest",
        "description": "Body for `POST /templates/{templateId}/duplicate`.\n\nBoth fields are optional; `{}` is a valid body.\n\nVERSIONS ONLY — never the working draft. A draft is mutable and unversioned, so there is no\nstable thing for an API client to reference (\"the draft as of now\" isn't expressible) and it\nmay be a teammate's mid-edit graph. So `version` omitted resolves to the published version if\none is pinned, else the latest SAVED version. This is the one place the API deliberately\ndiffers from the in-app menu, which forks the draft because the user can see it.",
        "example": {
          "name": "binder-design v2 experiment",
          "version": "v2"
        }
      },
      "PublicFastaMode": {
        "type": "string",
        "enum": [
          "one-entity-per-file",
          "one-entity-per-header"
        ],
        "title": "PublicFastaMode",
        "description": "The public spelling of the ingestion worker's FASTA split modes."
      },
      "PublicFieldType": {
        "type": "string",
        "enum": [
          "string",
          "integer",
          "float",
          "boolean",
          "category",
          "chain"
        ],
        "title": "PublicFieldType",
        "description": "The spec's `FieldType`.\n\nNOTE `chain` is a MEMBER of this enum, not a separate axis: a schema's `fields`\nlist interleaves scalar fields and chain fields, and `type == \"chain\"` is what\ndistinguishes them."
      },
      "PublicFileBinding": {
        "properties": {
          "file": {
            "type": "string",
            "title": "File",
            "description": "A file path (relative to your user folder)."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "file"
        ],
        "title": "PublicFileBinding"
      },
      "PublicFileFormat": {
        "type": "string",
        "enum": [
          "auto",
          "csv",
          "fasta",
          "sdf",
          "pdb",
          "zip",
          "sdf_zip"
        ],
        "title": "PublicFileFormat",
        "description": "The spec's `FileFormat` — a STRICT SUBSET of the internal\n`MoleculeFileFormat`, which also carries `cif`/`mmcif`. The public API does not\ndocument those, so they aren't accepted here; `auto` still detects anything the\nworker can read."
      },
      "PublicFileImportRequest": {
        "properties": {
          "groupId": {
            "type": "string",
            "maxLength": 200,
            "minLength": 1,
            "title": "Groupid"
          },
          "fileName": {
            "type": "string",
            "maxLength": 512,
            "minLength": 1,
            "title": "Filename"
          },
          "fileFormat": {
            "$ref": "#/components/schemas/PublicFileFormat",
            "default": "auto"
          },
          "sizeBytes": {
            "anyOf": [
              {
                "type": "integer",
                "maximum": 2147483648,
                "minimum": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Sizebytes",
            "description": "CSV imports (including `fileFormat: auto` with a `.csv` filename) may be up to 2 GiB. All other formats, including files left for content-based auto-detection, are limited to 16 MiB."
          },
          "columnMapping": {
            "anyOf": [
              {
                "additionalProperties": {
                  "type": "string"
                },
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Columnmapping"
          },
          "chainMapping": {
            "anyOf": [
              {
                "additionalProperties": {
                  "$ref": "#/components/schemas/PublicChainMappingEntry"
                },
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Chainmapping"
          },
          "fastaMode": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PublicFastaMode"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "groupId",
          "fileName"
        ],
        "title": "PublicFileImportRequest",
        "description": "The spec's `FileImportRequest`."
      },
      "PublicFileImportStart": {
        "properties": {
          "importId": {
            "type": "string",
            "title": "Importid"
          },
          "uploadUrl": {
            "type": "string",
            "title": "Uploadurl"
          },
          "uploadMethod": {
            "type": "string",
            "const": "PUT",
            "title": "Uploadmethod",
            "default": "PUT"
          },
          "uploadHeaders": {
            "additionalProperties": {
              "type": "string"
            },
            "type": "object",
            "title": "Uploadheaders"
          },
          "groupName": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Groupname"
          },
          "expiresInSeconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expiresinseconds"
          }
        },
        "type": "object",
        "required": [
          "importId",
          "uploadUrl",
          "uploadHeaders",
          "groupName",
          "expiresInSeconds"
        ],
        "title": "PublicFileImportStart"
      },
      "PublicGroup": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "displayName": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Displayname"
          },
          "matchedOn": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Matchedon"
          },
          "type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Type"
          },
          "status": {
            "type": "string",
            "title": "Status"
          },
          "moleculeCount": {
            "type": "integer",
            "title": "Moleculecount"
          },
          "schemaId": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Schemaid"
          },
          "source": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PublicGroupSource"
              },
              {
                "type": "null"
              }
            ]
          },
          "tags": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Tags"
          },
          "metadata": {
            "additionalProperties": true,
            "type": "object",
            "title": "Metadata"
          },
          "createdAt": {
            "type": "string",
            "title": "Createdat"
          },
          "updatedAt": {
            "type": "string",
            "title": "Updatedat"
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "displayName",
          "matchedOn",
          "type",
          "status",
          "moleculeCount",
          "schemaId",
          "source",
          "tags",
          "metadata",
          "createdAt",
          "updatedAt"
        ],
        "title": "PublicGroup",
        "description": "The spec's `Group` — a named collection of molecules.\n\nMolecule-only vocabulary: `moleculeCount`, never the internal `complexCount`."
      },
      "PublicGroupPage": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/PublicGroup"
            },
            "type": "array",
            "title": "Items"
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Nextcursor"
          }
        },
        "type": "object",
        "required": [
          "items",
          "nextCursor"
        ],
        "title": "PublicGroupPage"
      },
      "PublicGroupSource": {
        "properties": {
          "jobId": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Jobid"
          },
          "toolName": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Toolname"
          }
        },
        "type": "object",
        "required": [
          "jobId",
          "toolName"
        ],
        "title": "PublicGroupSource",
        "description": "`Group.source` — set when the group is a job's output."
      },
      "PublicImportStatus": {
        "properties": {
          "importId": {
            "type": "string",
            "title": "Importid"
          },
          "status": {
            "type": "string",
            "title": "Status"
          },
          "groupId": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Groupid"
          },
          "groupName": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Groupname"
          },
          "fileName": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Filename"
          },
          "fileFormat": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Fileformat"
          },
          "moleculeIds": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Moleculeids"
          },
          "moleculeCount": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Moleculecount"
          }
        },
        "type": "object",
        "required": [
          "importId",
          "status",
          "groupId",
          "groupName",
          "fileName",
          "fileFormat",
          "moleculeIds",
          "moleculeCount"
        ],
        "title": "PublicImportStatus"
      },
      "PublicInputSlot": {
        "properties": {
          "node": {
            "type": "string",
            "title": "Node",
            "description": "The stable input-node id you bind to."
          },
          "flow": {
            "$ref": "#/components/schemas/Flow"
          },
          "moleculeType": {
            "$ref": "#/components/schemas/MoleculeClass"
          },
          "requiresStructure": {
            "type": "boolean",
            "title": "Requiresstructure"
          },
          "label": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Label"
          },
          "chains": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Chains"
          },
          "chainLabels": {
            "additionalProperties": {
              "type": "string"
            },
            "type": "object",
            "title": "Chainlabels"
          },
          "residueFields": {
            "items": {
              "$ref": "#/components/schemas/PublicResidueField"
            },
            "type": "array",
            "title": "Residuefields"
          }
        },
        "type": "object",
        "required": [
          "node",
          "flow",
          "moleculeType",
          "requiresStructure",
          "label",
          "chains",
          "chainLabels",
          "residueFields"
        ],
        "title": "PublicInputSlot"
      },
      "PublicMolecule": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Type"
          },
          "entity": {
            "additionalProperties": {
              "type": "string"
            },
            "type": "object",
            "title": "Entity",
            "description": "The molecule's chains: chain id -> the chain's sequence — amino acids for a protein, nucleotides for a nucleic acid, or a SMILES string for a small molecule. `chainMapping[<chain id>].type` says which."
          },
          "chainMapping": {
            "additionalProperties": {
              "$ref": "#/components/schemas/MoleculeChainInfo"
            },
            "type": "object",
            "title": "Chainmapping",
            "description": "Per-chain detail, keyed by the same chain ids as `entity`: its `type` and any `tags`."
          },
          "files": {
            "additionalProperties": {
              "items": {
                "$ref": "#/components/schemas/MoleculeFileEntry"
              },
              "type": "array"
            },
            "type": "object",
            "title": "Files",
            "description": "Structures and other artifacts, keyed by the PRODUCER that made them — a tool name (`alphafold`, `protenix`) or `user` for your own uploads. Each entry carries a short-lived signed `downloadUrl`. This is where `hasStructure: true` resolves to an actual file. It can be EMPTY on the org-wide listing even for a molecule that has a structure: with no group to relabel a chain-bearing file's chains against, those files are served without a link and dropped — pass `group`, or read the molecule by id."
          },
          "metadata": {
            "additionalProperties": true,
            "type": "object",
            "title": "Metadata",
            "description": "TWO namespaces in one map, and knowing which is which is what makes `filter` work.\n\nA key naming a TOOL holds a LIST of that tool's score runs — `{\"protenix\": [{\"jobId\": \"...\", \"runId\": \"...\", \"ipSAE_AB\": 0.62}]}`. Any other key is a SCALAR annotation you set yourself — `{\"affinity_nM\": 12.4}`.\n\nOne carve-out: a key naming a COMPUTED schema field holds that formula's value, evaluated on read and never stored. It is READ-ONLY — you cannot set it here — and `filter`/`sortBy` do not address it, because they resolve against stored values and a computed field has none.\n\nFilter and sort address the two writable namespaces differently: a tool score is the DOTTED path `protenix.ipSAE_AB` (`?filter=protenix.ipSAE_AB:gte:0.5`), and a scalar is the BARE key (`?filter=affinity_nM:lte:50`). Metric names are case-sensitive and must match what you see here. `exists` applies to scalar keys only; to ask whether a tool reported a metric, filter on its value instead.\n\nEach run entry carries `jobId` AND `runId`, which answer different questions. `jobId` is the DISPATCH that produced the entry, and the engine dispatches one job per molecule — so the same run has a different `jobId` on every molecule and grouping a page by it yields one group per molecule. `runId` is the run — the pipeline NODE RUN, where there is one — and is the field to group by. Pass it straight back as `nodeRunId` to list everything that node run produced; the `nr-` prefix a pipeline run carries is accepted as-is, and `nodeRuns[].id` (the bare uuid) names the same run."
          },
          "truncated": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Truncated",
            "description": "Which inline collections were CAPPED for this molecule, so you know you are not looking at all of it: `\"scores\"` (more score runs than returned), `\"files\"`, or `\"groups\"` (page the rest via `GET /molecules/{moleculeId}/groups`). Empty means the maps above are the molecule's full accumulated history."
          },
          "createdAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Createdat"
          },
          "addedAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Addedat"
          },
          "createdBy": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Createdby"
          },
          "origin": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PublicMoleculeOrigin"
              },
              {
                "type": "null"
              }
            ]
          },
          "source": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Source"
          },
          "groups": {
            "items": {
              "$ref": "#/components/schemas/PublicGroup"
            },
            "type": "array",
            "title": "Groups"
          },
          "tags": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Tags"
          },
          "notes": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Notes"
          },
          "hasStructure": {
            "type": "boolean",
            "title": "Hasstructure",
            "description": "Whether this molecule has a structure at all (a pdb/cif, or an sdf). The files themselves are in `files`, keyed by the tool that produced them — but see that field: on the org-wide listing `files` can be empty even when this is true, because chain-bearing files need a group to relabel against. Pass `group`, or read the molecule by id, to get the download."
          },
          "matchedOn": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Matchedon"
          },
          "sortGroup": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PublicMoleculeSortGroup"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "type",
          "entity",
          "chainMapping",
          "files",
          "metadata",
          "truncated",
          "createdAt",
          "addedAt",
          "createdBy",
          "origin",
          "source",
          "groups",
          "tags",
          "notes",
          "hasStructure",
          "matchedOn"
        ],
        "title": "PublicMolecule",
        "description": "One molecule, everything inline — no follow-up call to read scores."
      },
      "PublicMoleculeBinding": {
        "properties": {
          "group": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Group",
            "description": "An existing molecules group id."
          },
          "sequences": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sequences",
            "description": "Raw protein or nucleic-acid sequences to make a group from."
          },
          "smiles": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Smiles",
            "description": "Raw small-molecule SMILES to make a group from."
          },
          "pdbs": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Pdbs",
            "description": "Uploaded .pdb file paths (relative to your user folder) to make a protein or nucleic-acid group from."
          },
          "sdfs": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sdfs",
            "description": "Uploaded .sdf file paths (relative to your user folder) to make a group from."
          },
          "name": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 200
              },
              {
                "type": "null"
              }
            ],
            "title": "Name",
            "description": "Name for the group created from raw values (auto if omitted)."
          },
          "chainMapping": {
            "anyOf": [
              {
                "additionalProperties": {
                  "type": "string"
                },
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Chainmapping",
            "description": "Optional; defaults to identity. referenceChain -> yourChain — only needed for an existing template whose reference chain IDs differ from your molecule's (for an inline pipeline your molecule IS the reference, so omit it)."
          },
          "residuesByChain": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Residuesbychain",
            "description": "Residue selection for this input, in ONE of two shapes (auto-detected by the value: a reference chain maps to a range STRING => SIMPLE; a tool node id maps to a per-field OBJECT => ADVANCED):\n- SIMPLE fan-out: `{ referenceChain -> ranges }` (e.g. `{\"A\": \"1-76\"}`). ONE selection applied to every residue field this input feeds. Used when the template maker turned on 'Simplified residue selection'.\n- ADVANCED per-field: `{ toolNodeId -> { field -> { referenceChain -> ranges } } }` — pick residues independently per (tool node, field) that this input feeds; two settings on the same chain can differ. Advanced is the DEFAULT for newly created templates. Picks under each binding are merged across bindings into one map.\nEach range value is a string of space/comma-separated residue numbers and inclusive ranges, e.g. `\"1-76\"` or `\"42 43 44 58 59\"` or `\"10-20,45,60-64\"`."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "PublicMoleculeBinding",
        "description": "A molecule binding. Provide the molecules ONE of these ways (exactly one):\n\n- `group` — an existing molecules group id, OR\n- `sequences` — raw protein or nucleic-acid sequences, OR\n- `smiles` — raw small-molecule SMILES, OR\n- `pdbs` / `sdfs` — paths (relative to your user folder) of already-uploaded structure files.\n\nFor the raw-value forms the server creates a molecules group for you (optionally named via `name`),\nthen binds it — so you don't have to pre-create one. The target input node declares the polymer\ntype for sequences/PDBs; SMILES/SDFs require a small-molecule target."
      },
      "PublicMoleculeCapsPage": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/PublicMolecule"
            },
            "type": "array",
            "title": "Items"
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Nextcursor"
          },
          "mode": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Mode"
          },
          "scanProgress": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Scanprogress"
          },
          "sliceMatched": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Slicematched"
          },
          "sliceDelivered": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Slicedelivered"
          },
          "waiting": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Waiting"
          },
          "retryAfterMs": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Retryafterms"
          },
          "caps": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Caps"
          }
        },
        "type": "object",
        "required": [
          "items",
          "nextCursor",
          "mode",
          "scanProgress",
          "caps"
        ],
        "title": "PublicMoleculeCapsPage"
      },
      "PublicMoleculeCard": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Type"
          },
          "entity": {
            "additionalProperties": {
              "type": "string"
            },
            "type": "object",
            "title": "Entity"
          },
          "matchedOn": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Matchedon"
          },
          "origin": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PublicMoleculeOrigin"
              },
              {
                "type": "null"
              }
            ]
          },
          "source": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Source"
          },
          "createdAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Createdat"
          },
          "addedAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Addedat"
          },
          "createdBy": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Createdby"
          },
          "tags": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Tags"
          },
          "notes": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Notes"
          },
          "hasStructure": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Hasstructure",
            "description": "Whether the molecule has a structure. `null` means the API does not yet know whether it has one."
          },
          "groups": {
            "items": {
              "$ref": "#/components/schemas/PublicGroup"
            },
            "type": "array",
            "title": "Groups"
          },
          "truncated": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Truncated"
          },
          "sortGroup": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PublicMoleculeSortGroup"
              },
              {
                "type": "null"
              }
            ]
          },
          "matchDetail": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PublicMoleculeMatchDetail"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "type",
          "entity",
          "matchedOn",
          "origin",
          "source",
          "createdAt",
          "addedAt",
          "createdBy",
          "tags",
          "notes",
          "hasStructure",
          "groups",
          "truncated",
          "sortGroup",
          "matchDetail"
        ],
        "title": "PublicMoleculeCard"
      },
      "PublicMoleculeCardCapsPage": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/PublicMoleculeCard"
            },
            "type": "array",
            "title": "Items"
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Nextcursor"
          },
          "mode": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Mode"
          },
          "scanProgress": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Scanprogress"
          },
          "sliceMatched": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Slicematched"
          },
          "sliceDelivered": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Slicedelivered"
          },
          "waiting": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Waiting"
          },
          "retryAfterMs": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Retryafterms"
          },
          "caps": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Caps"
          }
        },
        "type": "object",
        "required": [
          "items",
          "nextCursor",
          "mode",
          "scanProgress",
          "sliceMatched",
          "sliceDelivered",
          "waiting",
          "retryAfterMs",
          "caps"
        ],
        "title": "PublicMoleculeCardCapsPage"
      },
      "PublicMoleculeCardPage": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/PublicMoleculeCard"
            },
            "type": "array",
            "title": "Items"
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Nextcursor"
          },
          "mode": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Mode"
          },
          "scanProgress": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Scanprogress"
          },
          "sliceMatched": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Slicematched"
          },
          "sliceDelivered": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Slicedelivered"
          },
          "waiting": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Waiting"
          },
          "retryAfterMs": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Retryafterms"
          }
        },
        "type": "object",
        "required": [
          "items",
          "nextCursor",
          "mode",
          "scanProgress",
          "sliceMatched",
          "sliceDelivered",
          "waiting",
          "retryAfterMs"
        ],
        "title": "PublicMoleculeCardPage"
      },
      "PublicMoleculeInput": {
        "properties": {
          "name": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 500,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "type": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoleculeType"
              },
              {
                "type": "null"
              }
            ]
          },
          "entity": {
            "additionalProperties": {
              "type": "string"
            },
            "type": "object",
            "maxProperties": 64,
            "minProperties": 1,
            "title": "Entity"
          },
          "metadata": {
            "additionalProperties": true,
            "type": "object",
            "title": "Metadata"
          },
          "tags": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array",
                "maxItems": 64
              },
              {
                "type": "null"
              }
            ],
            "title": "Tags",
            "description": "Tags to add to the molecule. Existing tags are preserved; omission or an empty list leaves them unchanged."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "entity"
        ],
        "title": "PublicMoleculeInput",
        "description": "The spec's `MoleculeInput` — one molecule to create."
      },
      "PublicMoleculeMatchDetail": {
        "properties": {
          "axis": {
            "type": "string",
            "enum": [
              "property",
              "score"
            ],
            "title": "Axis"
          },
          "key": {
            "type": "string",
            "title": "Key"
          },
          "value": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Value"
          },
          "tool": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Tool"
          }
        },
        "type": "object",
        "required": [
          "axis",
          "key",
          "value",
          "tool"
        ],
        "title": "PublicMoleculeMatchDetail"
      },
      "PublicMoleculeOrigin": {
        "properties": {
          "type": {
            "type": "string",
            "title": "Type"
          },
          "jobId": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Jobid"
          },
          "jobType": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Jobtype"
          }
        },
        "type": "object",
        "required": [
          "type",
          "jobId",
          "jobType"
        ],
        "title": "PublicMoleculeOrigin",
        "description": "`Molecule.origin` — how this molecule came to exist.\n\n`type` is the `complexes.origin_type` provenance class and is ALWAYS present (an\nuploaded molecule is `user_import`). `jobId`/`jobType` name the job behind a tool-produced\nmolecule, `jobType` being the tool; for a plain batch submission `jobId` is the batch's own\nid rather than the run inside it, and it is that id `GET /molecules?jobId=` takes. A split\ndesign batch carries one of its runs' ids instead, as do molecules ingested before batches\nbegan naming the parent.\nBOTH are null for an upload — an upload has no job, and\nwe do not fabricate one. Read-only, projected from columns that already exist (no\nmigration)."
      },
      "PublicMoleculePage": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/PublicMolecule"
            },
            "type": "array",
            "title": "Items"
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Nextcursor"
          },
          "mode": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Mode"
          },
          "scanProgress": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Scanprogress"
          },
          "sliceMatched": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Slicematched"
          },
          "sliceDelivered": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Slicedelivered"
          },
          "waiting": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Waiting"
          },
          "retryAfterMs": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Retryafterms"
          }
        },
        "type": "object",
        "required": [
          "items",
          "nextCursor",
          "mode",
          "scanProgress"
        ],
        "title": "PublicMoleculePage",
        "description": "The spec's `MoleculePage` — `{items, nextCursor}` plus search-mode fields.\n\nDeliberately NOT `Page[PublicMolecule]`: the generic carries a field\n(`sortedServerSide`, the internal sheet's score-cap fallback flag) that the\npublished contract doesn't declare and no public caller can act on. The\nenvelope grew for `mode=sequence`, which is a CHUNKED scan and therefore has\nto say what a plain page cannot: which search actually ran, how far through\nthe scan this page got, and, inside a slice holding more matches than one\npage, how many of that slice's matches have been delivered so far."
      },
      "PublicMoleculeSortGroup": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          }
        },
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "title": "PublicMoleculeSortGroup",
        "description": "`Molecule.sortGroup` — the group this row was ORDERED BY, under `sortBy=groupName`.\n\nA molecule can be in many of your groups, so `sortBy=groupName` has to rank it by ONE\nof them: the alphabetically first EFFECTIVE (displayed) name across all your in-scope\nmemberships. That group is what makes a group's rows arrive contiguously, and it is\nthe group a grouped presentation should file the row under.\n\nIt is served because the inline `groups` list cannot be trusted to contain it: that\nlist is CAPPED and selected by membership recency, so a molecule in more groups than\nthe cap can be ranked by a group the response never carries. Re-deriving the section\nby name-sorting `groups` then files the row under the wrong heading — silently, and\nwith no way for a client to tell.\n\n`name` is the EFFECTIVE name (the rename label when one exists, else the canonical\none) — the exact string the ordering compared, so a section header built from it\ncannot disagree with the position the row was served in. That makes it the\n`displayName`-preferring sibling of `Group.name`, which is always the raw canonical\ncolumn; for a group that was never renamed the two are identical."
      },
      "PublicNodeRun": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "nodeId": {
            "type": "string",
            "title": "Nodeid",
            "description": "The stable pipeline node id."
          },
          "label": {
            "type": "string",
            "title": "Label"
          },
          "nodeType": {
            "type": "string",
            "title": "Nodetype"
          },
          "status": {
            "$ref": "#/components/schemas/NodeRunStatus"
          },
          "startedAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Startedat"
          },
          "completedAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Completedat"
          },
          "outputCount": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Outputcount"
          },
          "jobsTotal": {
            "type": "integer",
            "title": "Jobstotal"
          },
          "jobsComplete": {
            "type": "integer",
            "title": "Jobscomplete"
          },
          "outputGroup": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Outputgroup",
            "description": "The molecules group id this node run produced."
          }
        },
        "type": "object",
        "required": [
          "id",
          "nodeId",
          "label",
          "nodeType",
          "status",
          "startedAt",
          "completedAt",
          "outputCount",
          "jobsTotal",
          "jobsComplete",
          "outputGroup"
        ],
        "title": "PublicNodeRun"
      },
      "PublicNodeRunMolecule": {
        "properties": {
          "complexId": {
            "type": "string",
            "title": "Complexid",
            "description": "The molecule id. Called `id` on the `/molecules` endpoints, which describe the same molecule in a richer shape — `GET /molecules/{id}`, or `GET /molecules?nodeRunId=` for this whole node run's outputs at once."
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "moleculeType": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Moleculetype",
            "description": "protein, small_molecule, nucleic_acid, …"
          },
          "sequence": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sequence",
            "description": "The ':'-joined chain sequences."
          },
          "scores": {
            "additionalProperties": true,
            "type": "object",
            "title": "Scores",
            "description": "Per-tool scores keyed by tool, e.g. `{\"protenix\": {\"ipSAE_AB\": 0.62, \"pDockQ_AB\": 0.41}}`. Metric names are case-sensitive. To THRESHOLD or RANK on one of these, use `GET /molecules?nodeRunId=` with `filter=protenix.ipSAE_AB:gte:0.5` and `sortBy=protenix.ipSAE_AB` — this endpoint returns them but does not filter or sort by them."
          },
          "hasStructure": {
            "type": "boolean",
            "title": "Hasstructure",
            "description": "Whether this molecule has a structure. The FILES are not on this shape — read them from `GET /molecules?nodeRunId=...&group=...` or `GET /molecules/{complexId}`, whose `files` map carries a signed `downloadUrl` per artifact."
          }
        },
        "type": "object",
        "required": [
          "complexId",
          "name",
          "moleculeType",
          "sequence",
          "scores",
          "hasStructure"
        ],
        "title": "PublicNodeRunMolecule",
        "description": "One molecule a node run produced.\n\nRead from the node run's passing outputs directly, NOT from `PublicNodeRun.outputGroup` — which\nis why this exists. A node run's `outputGroup` is the group the node minted, and a node that enriches its\ninputs in place (scoring, structure prediction) mints none: its molecules never moved, so they\nstay in the group they came from and `outputGroup` is correctly null. A filter node has no group\neither — its survivors exist only as outputs. Reading results by group therefore reports\n\"produced nothing\" for exactly the node runs that produced the most interesting thing."
      },
      "PublicNodeRunMoleculePage": {
        "properties": {
          "molecules": {
            "items": {
              "$ref": "#/components/schemas/PublicNodeRunMolecule"
            },
            "type": "array",
            "title": "Molecules"
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Nextcursor",
            "description": "Pass as `cursor` for the next page."
          },
          "inputGroupId": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Inputgroupid",
            "description": "The derived molecular upstream group id; it may be a superset of this node's inputs."
          },
          "inputGroupExact": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Inputgroupexact",
            "description": "Whether inputGroupId contains exactly the molecules this node processed. False means it is a superset; null means there is no single molecular upstream group."
          }
        },
        "type": "object",
        "required": [
          "molecules",
          "nextCursor",
          "inputGroupId",
          "inputGroupExact"
        ],
        "title": "PublicNodeRunMoleculePage"
      },
      "PublicPublishRequest": {
        "properties": {
          "version": {
            "type": "string",
            "minLength": 1,
            "title": "Version",
            "description": "The version handle to publish e.g. 'v1' (from a prior response's version)."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "version"
        ],
        "title": "PublicPublishRequest",
        "example": {
          "version": "v1"
        }
      },
      "PublicPublishResponse": {
        "properties": {
          "templateId": {
            "type": "string",
            "title": "Templateid"
          },
          "isPublished": {
            "type": "boolean",
            "title": "Ispublished"
          },
          "publishedVersion": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Publishedversion",
            "description": "The published version handle e.g. 'v1'."
          }
        },
        "type": "object",
        "required": [
          "templateId",
          "isPublished",
          "publishedVersion"
        ],
        "title": "PublicPublishResponse"
      },
      "PublicRemoveMoleculesFromGroupRequest": {
        "properties": {
          "groupId": {
            "type": "string",
            "maxLength": 200,
            "minLength": 1,
            "title": "Groupid"
          },
          "moleculeIds": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "maxItems": 1000,
            "minItems": 1,
            "title": "Moleculeids"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "groupId",
          "moleculeIds"
        ],
        "title": "PublicRemoveMoleculesFromGroupRequest",
        "description": "Body for `POST /molecules/remove`: the group to detach FROM plus the molecules\nto detach — the group id travels in the body alongside the ids. Same `moleculeIds` cap (maxItems, a real\nstatement bound — every id is a bound `uuid[]` parameter, never inlined)."
      },
      "PublicRemoveMoleculesResponse": {
        "properties": {
          "removedIds": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Removedids"
          },
          "removedCount": {
            "type": "integer",
            "title": "Removedcount"
          }
        },
        "type": "object",
        "required": [
          "removedIds",
          "removedCount"
        ],
        "title": "PublicRemoveMoleculesResponse"
      },
      "PublicResidueField": {
        "properties": {
          "node": {
            "type": "string",
            "title": "Node"
          },
          "field": {
            "type": "string",
            "title": "Field"
          },
          "multichain": {
            "type": "boolean",
            "title": "Multichain"
          },
          "targetsChains": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Targetschains"
          }
        },
        "type": "object",
        "required": [
          "node",
          "field",
          "multichain",
          "targetsChains"
        ],
        "title": "PublicResidueField"
      },
      "PublicRun": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "templateId": {
            "type": "string",
            "title": "Templateid"
          },
          "templateVersion": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Templateversion",
            "description": "The executed version handle e.g. 'v1'."
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          },
          "status": {
            "$ref": "#/components/schemas/RunStatus"
          },
          "startedAt": {
            "type": "string",
            "title": "Startedat"
          },
          "completedAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Completedat"
          },
          "inputs": {
            "additionalProperties": true,
            "type": "object",
            "title": "Inputs",
            "description": "The recorded inputs (input-node id -> {group} or {file})."
          },
          "nodeRuns": {
            "items": {
              "$ref": "#/components/schemas/PublicNodeRun"
            },
            "type": "array",
            "title": "Noderuns"
          }
        },
        "type": "object",
        "required": [
          "id",
          "templateId",
          "templateVersion",
          "name",
          "source",
          "status",
          "startedAt",
          "completedAt",
          "inputs",
          "nodeRuns"
        ],
        "title": "PublicRun"
      },
      "PublicRunPage": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/PublicRunSummary"
            },
            "type": "array",
            "title": "Items"
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Nextcursor"
          }
        },
        "type": "object",
        "required": [
          "items",
          "nextCursor"
        ],
        "title": "PublicRunPage"
      },
      "PublicRunResults": {
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "ready",
              "failed"
            ],
            "title": "Status",
            "description": "`processing` (building / not finished), `ready` (download `url` set), or `failed`."
          },
          "url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Url",
            "description": "A short-lived signed download URL — present only when `status` is `ready`."
          },
          "nodeRunId": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Noderunid",
            "description": "The scoped node-run id, echoed back when `?nodeRunId=` was supplied."
          }
        },
        "type": "object",
        "required": [
          "status",
          "url",
          "nodeRunId"
        ],
        "title": "PublicRunResults",
        "description": "`GET /runs/results` — the run's (or one node's) output ZIP, produced asynchronously.\n\nPoll this: `processing` while the archive is being built (or the node run isn't finished yet),\n`ready` with a short-lived signed `url` once it's available, `failed` if the build failed. Polling\nis idempotent — it never starts a duplicate build.\n\n`url`/`nodeRunId` carry NO default (this surface's required-at-construction convention, see the\nmodule docstring): both are always present in the response — `null` when not applicable (`url`\nunless `ready`; `nodeRunId` unless a node run was requested) — so a client can rely on the\nstable key set."
      },
      "PublicRunSummary": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "templateId": {
            "type": "string",
            "title": "Templateid"
          },
          "templateVersion": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Templateversion",
            "description": "The executed version handle e.g. 'v1'."
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          },
          "status": {
            "$ref": "#/components/schemas/RunStatus"
          },
          "startedAt": {
            "type": "string",
            "title": "Startedat"
          },
          "completedAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Completedat"
          }
        },
        "type": "object",
        "required": [
          "id",
          "templateId",
          "templateVersion",
          "name",
          "source",
          "status",
          "startedAt",
          "completedAt"
        ],
        "title": "PublicRunSummary"
      },
      "PublicSchema": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "fields": {
            "items": {
              "$ref": "#/components/schemas/PublicSchemaField"
            },
            "type": "array",
            "title": "Fields"
          },
          "createdAt": {
            "type": "string",
            "title": "Createdat"
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "fields",
          "createdAt"
        ],
        "title": "PublicSchema",
        "description": "The spec's `Schema`."
      },
      "PublicSchemaField": {
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200,
            "minLength": 1,
            "title": "Name"
          },
          "type": {
            "$ref": "#/components/schemas/PublicFieldType",
            "default": "string"
          },
          "required": {
            "type": "boolean",
            "title": "Required",
            "default": false
          },
          "description": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "units": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 200
              },
              {
                "type": "null"
              }
            ],
            "title": "Units"
          },
          "options": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array",
                "maxItems": 500
              },
              {
                "type": "null"
              }
            ],
            "title": "Options",
            "description": "Allowed values for a `category` field. REQUIRED (non-empty) when `type` is `category`, and must be omitted for every other type."
          },
          "chainType": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PublicChainType"
              },
              {
                "type": "null"
              }
            ]
          },
          "chainTag": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PublicChainTag"
              },
              {
                "type": "null"
              }
            ]
          },
          "formula": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 1000
              },
              {
                "type": "null"
              }
            ],
            "title": "Formula",
            "description": "Spreadsheet expression computing this field's value per molecule. A field with a `formula` is COMPUTED: it is evaluated on read and never stored, so a caller may not supply a value for it."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "name"
        ],
        "title": "PublicSchemaField",
        "description": "The spec's `SchemaField` — one field in a schema.\n\nA RequestModel (extra='forbid') even though it also appears on responses: the\nfield names are already the wire spelling, so no alias generator is needed, and\nforbidding extras means a typo'd `chaintype` 422s at create instead of being\nsilently stored in the JSONB and never enforced.\n\n`type` defaults to `string` per the spec. `chain` is one of its values — a\nchain field's `name` IS the chain id (`H`, `L`, `A`), matching the keys of a\nmolecule's `entity` map."
      },
      "PublicSchemaPage": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/PublicSchema"
            },
            "type": "array",
            "title": "Items"
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Nextcursor"
          }
        },
        "type": "object",
        "required": [
          "items",
          "nextCursor"
        ],
        "title": "PublicSchemaPage"
      },
      "PublicScoreObservation": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "moleculeId": {
            "type": "string",
            "title": "Moleculeid"
          },
          "scoreRunId": {
            "type": "string",
            "title": "Scorerunid"
          },
          "tool": {
            "type": "string",
            "title": "Tool"
          },
          "jobId": {
            "type": "string",
            "title": "Jobid"
          },
          "jobName": {
            "type": "string",
            "title": "Jobname"
          },
          "dimensions": {
            "additionalProperties": true,
            "type": "object",
            "title": "Dimensions"
          },
          "metrics": {
            "additionalProperties": true,
            "type": "object",
            "title": "Metrics"
          },
          "structureFileId": {
            "type": "string",
            "title": "Structurefileid"
          },
          "selected": {
            "type": "boolean",
            "title": "Selected"
          },
          "createdAt": {
            "type": "string",
            "title": "Createdat"
          }
        },
        "type": "object",
        "required": [
          "id",
          "moleculeId",
          "scoreRunId",
          "tool",
          "jobId",
          "jobName",
          "dimensions",
          "metrics",
          "structureFileId",
          "selected",
          "createdAt"
        ],
        "title": "PublicScoreObservation",
        "description": "One retained prediction beneath a molecule's score run."
      },
      "PublicScoreObservationPage": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/PublicScoreObservation"
            },
            "type": "array",
            "title": "Items"
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Nextcursor"
          }
        },
        "type": "object",
        "required": [
          "items",
          "nextCursor"
        ],
        "title": "PublicScoreObservationPage"
      },
      "PublicScoreObservationQuery": {
        "properties": {
          "jobIds": {
            "items": {
              "type": "string",
              "maxLength": 200,
              "minLength": 1
            },
            "type": "array",
            "maxItems": 100,
            "minItems": 1,
            "title": "Jobids"
          },
          "tool": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 200,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Tool"
          },
          "limit": {
            "type": "integer",
            "maximum": 200,
            "minimum": 1,
            "title": "Limit",
            "default": 200
          },
          "cursor": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 4096,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Cursor"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "jobIds"
        ],
        "title": "PublicScoreObservationQuery",
        "description": "One bounded, explicit concrete-job observation read."
      },
      "PublicSubmitPipelineRequest": {
        "properties": {
          "pipeline": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PipelineIR"
              },
              {
                "type": "null"
              }
            ],
            "description": "INLINE mode: a full pipeline IR. Mutually exclusive with `templateId`."
          },
          "templateId": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Templateid",
            "description": "REFERENCE mode: an existing template id to run."
          },
          "name": {
            "type": "string",
            "maxLength": 200,
            "minLength": 1,
            "title": "Name",
            "description": "Name for this pipeline. When `runName` is omitted, the run's JobName is derived from this (spaces removed, a random suffix appended for uniqueness)."
          },
          "runName": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 200,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Runname",
            "description": "Optional explicit JobName for THIS run. Spaces are replaced with underscores; used verbatim otherwise (no random suffix). Omit to auto-generate a unique JobName from `name`."
          },
          "version": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Version",
            "description": "REFERENCE mode: a version handle e.g. 'v1'; absent -> default."
          },
          "bindings": {
            "additionalProperties": {
              "$ref": "#/components/schemas/PublicBinding"
            },
            "type": "object",
            "maxProperties": 2048,
            "title": "Bindings",
            "description": "One binding per input slot, keyed by the slot's input-node id. Bindings may be defined as an array of sequences, smiles, or sdf/pdb files (uploaded using the /upload endpoint), or an existing molecule group id (created using /molecules/groups below)."
          },
          "settings": {
            "anyOf": [
              {
                "additionalProperties": {
                  "additionalProperties": true,
                  "type": "object"
                },
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Settings",
            "description": "Per-node settings `{nodeId: {settingKey: value}}`. Chain and residue selections may be supplied here alongside ordinary settings. For existing templates, ordinary settings must be editable. Chain labels use reference space and follow chainMapping; inline residue selections must agree with overlapping binding selections."
          },
          "source": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Source"
              },
              {
                "type": "null"
              }
            ],
            "description": "Run source; defaults to production.",
            "default": "production"
          },
          "project": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Project",
            "description": "Organization project id to stamp on jobs this run creates."
          },
          "idempotencyKey": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 255,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Idempotencykey",
            "description": "Client key to safely retry a submit (≤255)."
          },
          "jobSource": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Jobsource",
            "description": "Submission channel: \"cli\", \"mcp\", or \"api\". Ignored for session-authenticated callers."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "name",
          "bindings"
        ],
        "title": "PublicSubmitPipelineRequest",
        "description": "Body for `POST /pipelines/submit` AND `POST /pipelines/validate` (identical shape). Two modes,\ndiscriminated by which of `pipeline` / `templateId` you send — exactly one is required:\n\n- INLINE: send `pipeline` (a full IR) — submit creates a template you own (unpublished) and runs\n  it; every setting is yours to set in the IR. `name` names both the created pipeline and the run.\n- REFERENCE: send `templateId` (+ optional `version`) — run an existing template; `settings`\n  overrides are limited to each tool node's `metadata.editableSettings`. `name` names the run.\n\n`name` is required and doubles as the run's display name (deduplicated per pipeline). `bindings` is\nrequired in both modes. `validate` runs the SAME body without executing/persisting.",
        "example": {
          "bindings": {
            "target": {
              "group": "3f8a1c2e-5b7d-4e9f-a1b2-c3d4e5f6a7b8"
            }
          },
          "name": "binder-design v2 experiment",
          "project": "proj_…",
          "settings": {
            "design": {
              "numSequences": 32
            }
          },
          "templateId": "3f2a1c9e-8b7d-4e6f-a1b2-c3d4e5f60718",
          "version": "v1"
        }
      },
      "PublicTemplate": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "isPublished": {
            "type": "boolean",
            "title": "Ispublished"
          },
          "publishedVersion": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Publishedversion",
            "description": "The published version handle e.g. 'v1' (null if none)."
          },
          "version": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Version",
            "description": "The version handle e.g. 'v1' (null if never saved)."
          },
          "simplifiedResidues": {
            "type": "boolean",
            "title": "Simplifiedresidues",
            "description": "True = one residue selection per binding fans to every field; False (default for new templates) = per (node, field) via a top-level residuesByChain."
          },
          "pipeline": {
            "$ref": "#/components/schemas/PipelineIR"
          },
          "inputs": {
            "items": {
              "$ref": "#/components/schemas/PublicInputSlot"
            },
            "type": "array",
            "title": "Inputs"
          },
          "versions": {
            "items": {
              "$ref": "#/components/schemas/PublicVersionSummary"
            },
            "type": "array",
            "title": "Versions",
            "description": "All saved versions, newest first."
          },
          "createdAt": {
            "type": "string",
            "title": "Createdat"
          },
          "updatedAt": {
            "type": "string",
            "title": "Updatedat"
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "description",
          "isPublished",
          "publishedVersion",
          "version",
          "simplifiedResidues",
          "pipeline",
          "inputs",
          "versions",
          "createdAt",
          "updatedAt"
        ],
        "title": "PublicTemplate"
      },
      "PublicTemplatePage": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/PublicTemplateSummary"
            },
            "type": "array",
            "title": "Items"
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Nextcursor"
          }
        },
        "type": "object",
        "required": [
          "items",
          "nextCursor"
        ],
        "title": "PublicTemplatePage"
      },
      "PublicTemplateSummary": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "isPublished": {
            "type": "boolean",
            "title": "Ispublished"
          },
          "versionCount": {
            "type": "integer",
            "title": "Versioncount"
          },
          "runCount": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Runcount"
          },
          "createdBy": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Createdby"
          },
          "createdAt": {
            "type": "string",
            "title": "Createdat"
          },
          "updatedAt": {
            "type": "string",
            "title": "Updatedat"
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "description",
          "isPublished",
          "versionCount",
          "runCount",
          "createdBy",
          "createdAt",
          "updatedAt"
        ],
        "title": "PublicTemplateSummary"
      },
      "PublicUpdateMetadataRequest": {
        "properties": {
          "properties": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Properties"
          },
          "source": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Source"
          },
          "fileId": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 200,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Fileid"
          },
          "name": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 200,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "metadata": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Metadata",
            "description": "DEPRECATED alias for `properties`, kept for callers written against the original v1 shape. Send `properties` instead; if both are sent, `properties` wins."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "PublicUpdateMetadataRequest",
        "description": "The spec's `UpdateMoleculeRequest` — a PARTIAL patch across the MOLECULE and\nMEMBERSHIP tenancy grains, applied ATOMICALLY (all-or-nothing, one transaction: if any\nfield's write fails, none of them persist).\n\nEvery field is OPTIONAL and independent: an ABSENT field is left untouched, which\nis DISTINCT from a field sent as `null` (which CLEARS it, where the grain allows).\nThe fields and the grain each writes:\n\n  * `properties` (MOLECULE grain) — merge scalar annotations into the molecule's\n    metadata. Only the keys you send change; a key set to `null` REMOVES that key\n    (an absent key and a null key mean different things, so the raw dict carries\n    intent). Tool score-run entries are written by tools and are not editable here;\n    when the molecule's group is schema-bound the MERGED result must still satisfy\n    it. This is the properties-only PATCH's behaviour, unchanged.\n  * `source` (MOLECULE grain) — the id of the PARENT molecule this one was derived\n    from; `null` clears it. The parent must be visible in YOUR scope, or the write\n    is refused (404) — you cannot point a molecule at a parent you cannot see.\n  * `fileId` (MOLECULE + MEMBERSHIP grain) — a structure file to associate as this\n    membership's primary; `null` clears it. The file must already be attached to\n    this molecule in your tenant.\n  * `name` (MEMBERSHIP grain) — rename this molecule's per-group display name. A\n    name already used by another molecule in the same group is a 409 conflict (the\n    `UNIQUE (group_id, name)` constraint), never a 500.\n\nThe membership-grained writes (`name`, `fileId`) target the molecule's MOST RECENT\nin-scope group membership, matching how the group-less by-id read resolves labels."
      },
      "PublicUpdateSchemaRequest": {
        "properties": {
          "name": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 200,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "fields": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/PublicSchemaField"
                },
                "type": "array",
                "maxItems": 200,
                "minItems": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Fields"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "PublicUpdateSchemaRequest",
        "description": "The spec's `UpdateSchemaRequest` — partial. `fields` REPLACES the whole\nlist; omitted keys are left unchanged."
      },
      "PublicUploadRequest": {
        "properties": {
          "groupId": {
            "type": "string",
            "maxLength": 200,
            "minLength": 1,
            "title": "Groupid"
          },
          "chainMapping": {
            "additionalProperties": {
              "$ref": "#/components/schemas/PublicChainMappingEntry"
            },
            "type": "object",
            "title": "Chainmapping"
          },
          "molecules": {
            "items": {
              "$ref": "#/components/schemas/PublicMoleculeInput"
            },
            "type": "array",
            "maxItems": 1000,
            "minItems": 1,
            "title": "Molecules"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "groupId",
          "molecules"
        ],
        "title": "PublicUploadRequest",
        "description": "The spec's `UploadRequest`."
      },
      "PublicUploadResponse": {
        "properties": {
          "groupId": {
            "type": "string",
            "title": "Groupid"
          },
          "groupName": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Groupname"
          },
          "moleculeIds": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Moleculeids"
          },
          "moleculeCount": {
            "type": "integer",
            "title": "Moleculecount"
          },
          "status": {
            "type": "string",
            "const": "pending",
            "title": "Status",
            "default": "pending"
          },
          "importId": {
            "type": "string",
            "title": "Importid"
          }
        },
        "type": "object",
        "required": [
          "groupId",
          "groupName",
          "moleculeIds",
          "moleculeCount",
          "importId"
        ],
        "title": "PublicUploadResponse"
      },
      "PublicValidateResponse": {
        "properties": {
          "valid": {
            "type": "boolean",
            "title": "Valid"
          },
          "errors": {
            "items": {
              "$ref": "#/components/schemas/PublicDiagnostic"
            },
            "type": "array",
            "title": "Errors"
          }
        },
        "type": "object",
        "required": [
          "valid",
          "errors"
        ],
        "title": "PublicValidateResponse"
      },
      "PublicValidateTemplateRequest": {
        "properties": {
          "version": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Version",
            "description": "A version handle e.g. 'v1' to validate; absent -> default."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "PublicValidateTemplateRequest",
        "description": "Body for `POST /templates/{id}/validate` — validate the TEMPLATE ITSELF (no run bindings),\nagainst its own `metadata.defaultGroup` reference groups. `{}` is valid (validate the default\nversion)."
      },
      "PublicVersionSummary": {
        "properties": {
          "version": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Version",
            "description": "The version handle e.g. 'v1'."
          },
          "createdAt": {
            "type": "string",
            "title": "Createdat"
          },
          "isPublished": {
            "type": "boolean",
            "title": "Ispublished"
          },
          "isValid": {
            "type": "boolean",
            "title": "Isvalid"
          }
        },
        "type": "object",
        "required": [
          "version",
          "createdAt",
          "isPublished",
          "isValid"
        ],
        "title": "PublicVersionSummary"
      },
      "RunStatus": {
        "type": "string",
        "enum": [
          "queued",
          "running",
          "finished",
          "partial",
          "stopped",
          "failed"
        ],
        "title": "RunStatus"
      },
      "Severity": {
        "type": "string",
        "enum": [
          "error",
          "warning"
        ],
        "title": "Severity"
      },
      "Source": {
        "type": "string",
        "enum": [
          "production",
          "test"
        ],
        "title": "Source"
      },
      "PublicProblem": {
        "additionalProperties": false,
        "description": "RFC 9457 problem detail. Serialized as `application/problem+json` on every public error.",
        "properties": {
          "type": {
            "description": "A URI identifying the error kind (dereferenceable docs).",
            "title": "Type",
            "type": "string"
          },
          "title": {
            "description": "A short, human-readable summary of the error kind.",
            "title": "Title",
            "type": "string"
          },
          "status": {
            "description": "The HTTP status code, duplicated in the body per RFC 9457.",
            "title": "Status",
            "type": "integer"
          },
          "code": {
            "description": "A stable machine-readable slug; switch on THIS, not prose.",
            "title": "Code",
            "type": "string"
          },
          "detail": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Instance-specific human explanation.",
            "title": "Detail"
          },
          "errors": {
            "anyOf": [
              {
                "items": {
                  "additionalProperties": true,
                  "type": "object"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Structured per-item detail (request-validation fields OR pipeline diagnostics).",
            "title": "Errors"
          },
          "retryable": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Whether the SAME request can succeed later. `true` means retry (honour `Retry-After` when present); `false` means this will not resolve on its own and retrying cannot change it. Omitted when the error does not specify — absence is NOT `false`.",
            "title": "Retryable"
          }
        },
        "required": [
          "type",
          "title",
          "status",
          "code"
        ],
        "title": "PublicProblem",
        "type": "object"
      },
      "ProjectJobPage": {
        "type": "object",
        "title": "ProjectJobPage",
        "description": "One page of a project's jobs: the response when `projectId` is sent.",
        "additionalProperties": false,
        "required": [
          "projectId",
          "jobs",
          "statuses"
        ],
        "properties": {
          "projectId": {
            "type": "string",
            "description": "The requested `projectId`, echoed. A listing without it was not filtered by project."
          },
          "jobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectJobSummary"
            }
          },
          "startKey": {
            "type": "string",
            "description": "Present only when another page follows. Send it back as `startKey` to get it."
          },
          "statuses": {
            "type": "object",
            "description": "How many jobs on THIS page are `Complete`, `In Queue`, `Running` and `Stopped`.",
            "additionalProperties": {
              "type": "integer"
            }
          }
        }
      },
      "ProjectJobSummary": {
        "type": "object",
        "title": "ProjectJobSummary",
        "additionalProperties": false,
        "required": [
          "JobName",
          "JobStatus",
          "Created",
          "Type",
          "User"
        ],
        "properties": {
          "JobName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Send as `jobName` to `PUT /api/updateJobProject`. Can be null or EMPTY: a submitted name is cleaned to ASCII letters, digits, `_`, `.` and `-`, so a name of other characters is stored as `\"\"`. Such a row is still a real job; it cannot be addressed by name, so move it in the app."
          },
          "JobStatus": {
            "type": [
              "string",
              "null"
            ],
            "description": "Null for a pipeline run; read its status by `pipelineRunId`."
          },
          "Created": {
            "type": "string",
            "description": "When the job was submitted."
          },
          "Type": {
            "type": [
              "string",
              "null"
            ],
            "description": "The tool the job ran."
          },
          "User": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who submitted the job."
          },
          "pipelineRunId": {
            "type": "string",
            "description": "Only on pipeline runs. Look the run up with `GET /api/pipelines/runs/{run_id}`."
          }
        }
      },
      "ProjectJobsError": {
        "type": "object",
        "title": "ProjectJobsError",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Present on some refusals, such as `MFA_REQUIRED` on a 403."
          },
          "retryable": {
            "type": "boolean",
            "description": "Present on a 503 when organization access is temporarily unavailable; true means retry."
          }
        }
      }
    },
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Your Tamarind API key. Create one at https://app.tamarind.bio/api-docs/api-key."
      }
    }
  },
  "info": {
    "title": "Tamarind Bio API",
    "version": "1.0",
    "description": "The complete Tamarind Bio public API.\n\n- **Submission, tools, results, and files** — under `/api`. Submit and manage runs of any tool in the catalog.\n- **Jobs reads** — under `/api/jobs`, preserving the established request and response contract.\n- **Pipelines and molecules** — typed resources under `/api/pipelines` and `/api/molecules`.\n\n**Use the host that served this spec.** `servers` is set to the deployment you\nfetched from. An organisation with a dedicated deployment keeps its jobs, files and\nAPI keys in its own account, reachable only from that host — and sending to the\nshared host from such an account does NOT error: the request succeeds and the work\nlands where that user will never see it.\n\nAlmost every request authenticates with an `x-api-key` header — get one at\n`/api-docs/api-key`. **Route families answer a bad key differently, so branch on\n\"not 2xx\" rather than on a status.** The classic submit/tools/files routes answer\n**400** with a JSON recovery body. The `/api/jobs` application contract declares **401**\nwith an RFC 9457 `application/problem+json` body. Jobs, pipelines and molecules share\ngateway authentication, which can reject bad credentials earlier with a generic **403**.\n\nSome classic routes are themselves exceptions to the 400 rule.\n`GET /finetuned-models` and `GET /usage-statistics` answer **401** with a bare scalar\n(`Unauthenticated`, `Unauthorized`) and no recovery fields. `GET /projects/list`,\n`POST /projects/create`, `PUT /projects/update`, `DELETE /projects/delete`,\n`GET /projects/members/list`, `POST /projects/members/leave` and\n`PUT /updateJobProject` answer **401** as `{\"error\": \"Unauthorized\"}`, also with no\nrecovery fields — they are session-first UI routes that gained the api-key path, and the\nbrowser relies on that status. So does `GET /api/jobs` when it carries `projectId`: that\nlisting is served beside the project routes, not by the Jobs application.\nEvery route under `/projects/{projectId}/notes` answers\n**401** with no recovery fields either, but as RFC 9457 problem+json.\n`POST /model-router` answers **401** with an RFC 9457\n`application/problem+json` body. The assay routes — `GET /assay-types`,\n`GET /assay-targets`, `POST /validate-assay`, `POST /estimate-assay`,\n`POST /submit-assay`, `GET /assays`, `POST /cancel-assay` and `GET /assay-results` —\nanswer **401** with the usual JSON body and its recovery fields.\n`PUT /upload/{filename}`\nis not served by the API layer at all — it redirects to a CloudFront host, so an\nunauthenticated caller receives the redirect rather than any JSON body.\n\n**Request bodies are size-capped, and the limit is per-route.** Under `/api`,\n1 MB is the default — a route that does not raise it explicitly gets it, and most do\nnot. Routes that need more raise it, up to a ceiling of ~4.5 MB that cannot be exceeded\nby declaring a larger value; `/submit-job`, `/submit-batch` and `/validate-job` are\nraised to it. A few enforce a smaller limit in their own code. Jobs, pipelines,\nmolecules and custom-tools operations have limits of their own and are not covered by\nthis default. An over-limit request is refused and NOTHING WAS CREATED — a 413\nfrom a submit endpoint never left work behind; what the refusal carries varies by\ndeployment and by route, so read a 413 like a 400 and do not branch on its shape.\nAny request-body operation can answer 413, so handle it once rather than per operation.\nSend file inputs by uploading them and referencing the path, never as inline content,\nand split a large batch across requests.\n\n**Exception: `GET /api/tools-catalog` (also `/tools.json`, `/tools.md`) needs NO key.**\nIt lists every publicly submittable tool `type` with its required settings, so a correct\npayload can be built before an account exists. This overview previously said every\nrequest needs a key, which sent agents away before trying the one open door."
  },
  "openapi": "3.1.0",
  "paths": {
    "/api/custom-tools": {
      "get": {
        "description": "List custom tools in your organization.\n\nFilter by status or publication state. Paginated — follow `nextCursor` until it is null.",
        "operationId": "listCustomTools",
        "parameters": [
          {
            "description": "Filter by status.",
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/PublicCustomToolStatus"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Filter by status.",
              "title": "Status"
            }
          },
          {
            "description": "Filter to published (`true`) or unpublished (`false`) tools.",
            "in": "query",
            "name": "published",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "boolean"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Filter to published (`true`) or unpublished (`false`) tools.",
              "title": "Published"
            }
          },
          {
            "description": "Maximum number of tools to return in one page.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 50,
              "description": "Maximum number of tools to return in one page.",
              "maximum": 200,
              "minimum": 1,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "description": "Pagination token from the previous response's `nextCursor`.",
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "maxLength": 4096,
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Pagination token from the previous response's `nextCursor`.",
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicCustomToolPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "$ref": "#/components/responses/CustomToolsUnauthorized"
          },
          "422": {
            "$ref": "#/components/responses/CustomToolsValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/CustomToolsError"
          }
        },
        "summary": "List custom tools",
        "tags": [
          "custom-tools"
        ],
        "x-doc-group": "Custom Tools"
      },
      "post": {
        "description": "Create a custom tool in Draft status.\n\nThis creates the tool record only. Upload its source and build a version in separate requests.",
        "operationId": "createCustomTool",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicCreateCustomToolRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicCustomTool"
                }
              }
            },
            "description": "The created Tool.",
            "headers": {
              "ETag": {
                "description": "ETag to send in If-Match when changing this tool.",
                "schema": {
                  "example": "\"01JABCDEF0123456789XYZ\"",
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/CustomToolsUnauthorized"
          },
          "422": {
            "$ref": "#/components/responses/CustomToolsValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/CustomToolsError"
          }
        },
        "summary": "Create a custom tool",
        "tags": [
          "custom-tools"
        ],
        "x-doc-group": "Custom Tools"
      }
    },
    "/api/custom-tools/{name}": {
      "delete": {
        "description": "Delete this custom tool and release its name for reuse.",
        "operationId": "deleteCustomTool",
        "parameters": [
          {
            "description": "The custom tool name.",
            "in": "path",
            "name": "name",
            "required": true,
            "schema": {
              "description": "The custom tool name.",
              "maxLength": 50,
              "minLength": 3,
              "pattern": "^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$",
              "title": "Name",
              "type": "string"
            }
          },
          {
            "description": "Optional: send the `etag` field returned in the Get tool JSON body, including its quotes, to avoid overwriting changes made since that read. Standard If-Match lists and `*` are also accepted; `*` checks only that the tool exists. Without this header, changes apply to the current tool state and may overwrite concurrent edits.",
            "in": "header",
            "name": "If-Match",
            "required": false,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "401": {
            "$ref": "#/components/responses/CustomToolsUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/CustomToolsNotFound"
          },
          "412": {
            "$ref": "#/components/responses/CustomToolsPreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/CustomToolsValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/CustomToolsError"
          }
        },
        "summary": "Delete a custom tool",
        "tags": [
          "custom-tools"
        ],
        "x-doc-group": "Custom Tools"
      },
      "get": {
        "description": "Get a custom tool and one version.\n\nBy default this includes the latest version. Pass `version=v2` to inspect a specific version.\nA tool that has never been built returns `version: null`.",
        "operationId": "getCustomTool",
        "parameters": [
          {
            "description": "The custom tool name.",
            "in": "path",
            "name": "name",
            "required": true,
            "schema": {
              "description": "The custom tool name.",
              "maxLength": 50,
              "minLength": 3,
              "pattern": "^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$",
              "title": "Name",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "version",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "description": "A version number such as `v3`. Omit it to use the endpoint's documented default.",
                  "maxLength": 32,
                  "minLength": 2,
                  "pattern": "^v[1-9][0-9]*$",
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Version"
            }
          },
          {
            "description": "Optional SDK-managed identity condition. A retained Tool or Version ETag prevents a read from silently switching to a replacement tool with the same name. Representation changes within the same resource remain readable.",
            "in": "header",
            "name": "If-Match",
            "required": false,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicCustomToolDetail"
                }
              }
            },
            "description": "The current custom tool."
          },
          "401": {
            "$ref": "#/components/responses/CustomToolsUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/CustomToolsNotFound"
          },
          "412": {
            "$ref": "#/components/responses/CustomToolsPreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/CustomToolsValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/CustomToolsError"
          }
        },
        "summary": "Get a custom tool",
        "tags": [
          "custom-tools"
        ],
        "x-doc-group": "Custom Tools"
      },
      "patch": {
        "description": "Update a custom tool's metadata or compute resources.\n\nFields you omit are left unchanged.",
        "operationId": "updateCustomTool",
        "parameters": [
          {
            "description": "The custom tool name.",
            "in": "path",
            "name": "name",
            "required": true,
            "schema": {
              "description": "The custom tool name.",
              "maxLength": 50,
              "minLength": 3,
              "pattern": "^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$",
              "title": "Name",
              "type": "string"
            }
          },
          {
            "description": "Optional: send the `etag` field returned in the Get tool JSON body, including its quotes, to avoid overwriting changes made since that read. Standard If-Match lists and `*` are also accepted; `*` checks only that the tool exists. Without this header, changes apply to the current tool state and may overwrite concurrent edits.",
            "in": "header",
            "name": "If-Match",
            "required": false,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicUpdateCustomToolRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicCustomTool"
                }
              }
            },
            "description": "The updated Tool.",
            "headers": {
              "ETag": {
                "description": "ETag to send in If-Match when changing this tool.",
                "schema": {
                  "example": "\"01JABCDEF0123456789XYZ\"",
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/CustomToolsUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/CustomToolsNotFound"
          },
          "412": {
            "$ref": "#/components/responses/CustomToolsPreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/CustomToolsValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/CustomToolsError"
          }
        },
        "summary": "Update a custom tool",
        "tags": [
          "custom-tools"
        ],
        "x-doc-group": "Custom Tools"
      }
    },
    "/api/custom-tools/{name}/build": {
      "post": {
        "description": "Build a version from an uploaded source archive.\n\nThe archive also updates the tool's editable source and configuration. Optionally send the Tool\n`etag` field from the Get tool JSON body to reject concurrent edits. Without it, the current tool is updated.\nReturns immediately with the new or reused version and its current status.",
        "operationId": "buildCustomTool",
        "parameters": [
          {
            "description": "The custom tool name.",
            "in": "path",
            "name": "name",
            "required": true,
            "schema": {
              "description": "The custom tool name.",
              "maxLength": 50,
              "minLength": 3,
              "pattern": "^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$",
              "title": "Name",
              "type": "string"
            }
          },
          {
            "description": "Retries the same build request without admitting duplicate work. Reusing a key with different source or runtime facts returns 409 Conflict.",
            "in": "header",
            "name": "Idempotency-Key",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "maxLength": 255,
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Retries the same build request without admitting duplicate work. Reusing a key with different source or runtime facts returns 409 Conflict.",
              "title": "Idempotency-Key"
            }
          },
          {
            "description": "Optional: send the `etag` field returned in the Get tool JSON body, including its quotes, to avoid overwriting changes made since that read. Standard If-Match lists and `*` are also accepted; `*` checks only that the tool exists. Without this header, changes apply to the current tool state and may overwrite concurrent edits.",
            "in": "header",
            "name": "If-Match",
            "required": false,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicCreateVersionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicBuildResult"
                }
              }
            },
            "description": "The admitted Version.",
            "headers": {
              "ETag": {
                "description": "Opaque validator retained by SDK Version resources for refreshes and safe mutations.",
                "schema": {
                  "type": "string"
                }
              },
              "Location": {
                "description": "Relative URL of the exact version admitted or reused by this request.",
                "schema": {
                  "example": "../my-custom-tool?version=v3",
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/CustomToolsUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/CustomToolsNotFound"
          },
          "409": {
            "$ref": "#/components/responses/CustomToolsConflict"
          },
          "412": {
            "$ref": "#/components/responses/CustomToolsPreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/CustomToolsValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/CustomToolsError"
          }
        },
        "summary": "Build a custom tool",
        "tags": [
          "custom-tools"
        ],
        "x-doc-group": "Custom Tools"
      }
    },
    "/api/custom-tools/{name}/build-logs": {
      "get": {
        "description": "List build log events for a version.\n\nBy default this reads the latest version. Pass `version=v2` for a specific version. Paginated —\nfollow `nextCursor` until it is null.",
        "operationId": "listCustomToolBuildLogs",
        "parameters": [
          {
            "description": "The custom tool name.",
            "in": "path",
            "name": "name",
            "required": true,
            "schema": {
              "description": "The custom tool name.",
              "maxLength": 50,
              "minLength": 3,
              "pattern": "^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$",
              "title": "Name",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "version",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "description": "A version number such as `v3`. Omit it to use the endpoint's documented default.",
                  "maxLength": 32,
                  "minLength": 2,
                  "pattern": "^v[1-9][0-9]*$",
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Version"
            }
          },
          {
            "description": "Pagination token from the previous response's `nextCursor`.",
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "maxLength": 4096,
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Pagination token from the previous response's `nextCursor`.",
              "title": "Cursor"
            }
          },
          {
            "description": "Optional SDK-managed identity condition. A retained Tool or Version ETag prevents a read from silently switching to a replacement tool with the same name. Representation changes within the same resource remain readable.",
            "in": "header",
            "name": "If-Match",
            "required": false,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicBuildLogPage"
                }
              }
            },
            "description": "Build events and current version status."
          },
          "401": {
            "$ref": "#/components/responses/CustomToolsUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/CustomToolsNotFound"
          },
          "412": {
            "$ref": "#/components/responses/CustomToolsPreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/CustomToolsValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/CustomToolsError"
          }
        },
        "summary": "List build logs",
        "tags": [
          "custom-tools"
        ],
        "x-doc-group": "Custom Tools"
      }
    },
    "/api/custom-tools/{name}/cancel-build": {
      "post": {
        "description": "Request cancellation of an active build.\n\nWhen exactly one build is active, no version is needed. If multiple builds are active, pass a\nversion such as `version=v2`. Cancellation is a request; continue polling for the outcome.",
        "operationId": "cancelCustomToolBuild",
        "parameters": [
          {
            "description": "The custom tool name.",
            "in": "path",
            "name": "name",
            "required": true,
            "schema": {
              "description": "The custom tool name.",
              "maxLength": 50,
              "minLength": 3,
              "pattern": "^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$",
              "title": "Name",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "version",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "description": "A version number such as `v3`. Omit it to use the endpoint's documented default.",
                  "maxLength": 32,
                  "minLength": 2,
                  "pattern": "^v[1-9][0-9]*$",
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Version"
            }
          },
          {
            "description": "Optional resource condition used by SDKs to prevent a stale version handle from changing a replacement tool with the same name. The SDK manages this value automatically.",
            "in": "header",
            "name": "If-Match",
            "required": false,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicVersion"
                }
              }
            },
            "description": "The version with cancellation requested.",
            "headers": {
              "ETag": {
                "description": "Opaque validator retained by SDK Version resources for refreshes and safe mutations.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/CustomToolsUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/CustomToolsNotFound"
          },
          "409": {
            "$ref": "#/components/responses/CustomToolsConflict"
          },
          "412": {
            "$ref": "#/components/responses/CustomToolsPreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/CustomToolsValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/CustomToolsError"
          }
        },
        "summary": "Cancel a custom tool build",
        "tags": [
          "custom-tools"
        ],
        "x-doc-group": "Custom Tools"
      }
    },
    "/api/custom-tools/{name}/publish": {
      "post": {
        "description": "Publish a completed version.\n\nBy default this publishes the latest completed version. Pass `version=v2` to publish or roll back\nto a specific completed version.",
        "operationId": "publishCustomTool",
        "parameters": [
          {
            "description": "The custom tool name.",
            "in": "path",
            "name": "name",
            "required": true,
            "schema": {
              "description": "The custom tool name.",
              "maxLength": 50,
              "minLength": 3,
              "pattern": "^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$",
              "title": "Name",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "version",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "description": "A version number such as `v3`. Omit it to use the endpoint's documented default.",
                  "maxLength": 32,
                  "minLength": 2,
                  "pattern": "^v[1-9][0-9]*$",
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Version"
            }
          },
          {
            "description": "Optional resource condition. Send the Tool ETag to protect its current representation. SDK Version resources may instead send the selected Version ETag to fence publication to that tool lifetime.",
            "in": "header",
            "name": "If-Match",
            "required": false,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicCustomTool"
                }
              }
            },
            "description": "The Tool with its published version updated.",
            "headers": {
              "ETag": {
                "description": "ETag to send in If-Match when changing this tool.",
                "schema": {
                  "example": "\"01JABCDEF0123456789XYZ\"",
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/CustomToolsUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/CustomToolsNotFound"
          },
          "412": {
            "$ref": "#/components/responses/CustomToolsPreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/CustomToolsValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/CustomToolsError"
          }
        },
        "summary": "Publish a custom tool version",
        "tags": [
          "custom-tools"
        ],
        "x-doc-group": "Custom Tools"
      }
    },
    "/api/custom-tools/{name}/uploads": {
      "post": {
        "description": "Create a temporary upload for a source archive.\n\nUpload the file to `uploadUrl` using `uploadMethod` and every value in `uploadHeaders`, then use\n`uploadId` to build a version.",
        "operationId": "createCustomToolUpload",
        "parameters": [
          {
            "description": "The custom tool name.",
            "in": "path",
            "name": "name",
            "required": true,
            "schema": {
              "description": "The custom tool name.",
              "maxLength": 50,
              "minLength": 3,
              "pattern": "^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$",
              "title": "Name",
              "type": "string"
            }
          },
          {
            "description": "Optionally send the `etag` field returned in the Get tool JSON body to reject a stale tool before uploading. To protect build admission too, also send it on the build request.",
            "in": "header",
            "name": "If-Match",
            "required": false,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicUploadSession"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "$ref": "#/components/responses/CustomToolsUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/CustomToolsNotFound"
          },
          "412": {
            "$ref": "#/components/responses/CustomToolsPreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/CustomToolsValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/CustomToolsError"
          }
        },
        "summary": "Create a source upload",
        "tags": [
          "custom-tools"
        ],
        "x-doc-group": "Custom Tools"
      }
    },
    "/api/custom-tools/{name}/versions": {
      "get": {
        "description": "List versions of a custom tool.\n\nFilter by build status. Paginated — follow `nextCursor` until it is null.",
        "operationId": "listCustomToolVersions",
        "parameters": [
          {
            "description": "The custom tool name.",
            "in": "path",
            "name": "name",
            "required": true,
            "schema": {
              "description": "The custom tool name.",
              "maxLength": 50,
              "minLength": 3,
              "pattern": "^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$",
              "title": "Name",
              "type": "string"
            }
          },
          {
            "description": "Filter by build status.",
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/PublicVersionStatus"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Filter by build status.",
              "title": "Status"
            }
          },
          {
            "description": "Maximum number of versions to return in one page.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 50,
              "description": "Maximum number of versions to return in one page.",
              "maximum": 200,
              "minimum": 1,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "description": "Pagination token from the previous response's `nextCursor`.",
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "maxLength": 4096,
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Pagination token from the previous response's `nextCursor`.",
              "title": "Cursor"
            }
          },
          {
            "description": "Optional SDK-managed identity condition. A retained Tool or Version ETag prevents a read from silently switching to a replacement tool with the same name. Representation changes within the same resource remain readable.",
            "in": "header",
            "name": "If-Match",
            "required": false,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicVersionPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "$ref": "#/components/responses/CustomToolsUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/CustomToolsNotFound"
          },
          "412": {
            "$ref": "#/components/responses/CustomToolsPreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/CustomToolsValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/CustomToolsError"
          }
        },
        "summary": "List custom tool versions",
        "tags": [
          "custom-tools"
        ],
        "x-doc-group": "Custom Tools"
      }
    },
    "/api/submit-job": {
      "post": {
        "summary": "Submit a single job",
        "description": "Submit a job using an available tool. Pipeline tools delegate to Jobs and require submitId; retain it and the unchanged body for retries. Their JSON results contain pipeline run IDs.\n\n\nThis is also how you RUN INFERENCE WITH A FINETUNED MODEL — there is no separate inference endpoint. List your models with `GET /finetuned-models`; each row's `inferenceType` is the tool to submit here as `type`, and its `id` goes in `modelId` (or its `name` in `modelName`). You can run a model you trained (`personalModels`) or one a teammate has shared with your organization (`organizationModels`), and only as its own `inferenceType`. A name always means your own model when you have one by that name, so send the `id` to run a teammate's. `settings` are that inference tool's own inputs, from `GET /tools/{name}/schema`. For example, a `chemprop-finetune` model runs as `{\"jobName\": \"my-predictions\", \"type\": \"chemprop-inference\", \"modelId\": \"<id>\", \"settings\": {\"smiles\": [\"CCO\"]}}`. Training a model is `POST /finetune`.",
        "operationId": "submitJob",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JobSubmission"
              },
              "example": {
                "jobName": "my-protein-analysis",
                "type": "alphafold",
                "settings": {
                  "sequence": "MKTVRQERLKSIVRILERSKEPVSGAQLAEELSVSRQVIVQDIAYLRSLGYNIVATPRGYVLAGG"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "For pipeline tools, the JSON body contains success, startedVia=\"pipeline\", and results with each run's Id and handoff. Inspect each handoff; HTTP 200 can be partial. For ordinary tools, the body is PLAIN TEXT, not JSON, and carries no job id — literally `<jobName> submitted to queue.` A generated client that calls .json() on this response throws on the HAPPY path. POLL THE NAME ECHOED IN THIS BODY, not the one you sent. `jobName` is NORMALIZED rather than validated: characters outside [A-Za-z0-9_.-] are stripped and whitespace becomes `_`, so submitting `my run!` stores `my_run` and polling `my run!` afterwards reports an unknown job. Parse the name out of this response and use that as the handle, or pre-normalize before submitting so the two cannot differ.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PipelineToolSubmissionResponse"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "my-protein-analysis submitted to queue."
                }
              }
            }
          },
          "400": {
            "description": "Bad request. TWO body shapes share this status code, and you cannot tell them apart by `Content-Type`: Next's `send()` emits a string body with NO `Content-Type` at all (see `sendSubmitted` in `pages/api/submit-job.js`). Try to parse the body as JSON — if it parses you have an `ErrorResponse`; if it does not, the body IS the message, in full.\nWhich rejections are JSON is NOT enumerated here, and deliberately: the list has been wrong every time it was written down. Parse, then read what you got. A JSON body may carry a machine-readable flag beside `error` — `availableProjects` on the required-project refusal, `getApiKey`/`agentGuide`/`toolCatalog` on an API-key rejection, `designCountTooLarge` and `batchingUnsupported` on custom-tool design caps — so branch on the FIELD you need rather than on a status or a media type. Most rejections are still a bare string, and for those the body is the whole message.\n\nFinetuning tools (`finetune: true` in `GET /tools`) are submitted with `POST /finetune`. This route may refuse one with JSON `{\"code\": \"use_finetune_endpoint\", \"endpoint\": \"/finetune\"}` — resend the same body there with the tool in `model` instead of `type`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "Missing required field \"jobName\""
                }
              }
            }
          },
          "413": {
            "description": "Request body over the API-wide ~4.5 MB cap. Generated ahead of this endpoint, so the body is empty and NO job was created. Almost always inline file content — upload the file and reference it by path instead."
          },
          "422": {
            "description": "Pipeline-tool submission forwards validation failures from the Jobs API. The upstream response body and content type are preserved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "tags": [
          "Jobs"
        ]
      }
    },
    "/api/submit-batch": {
      "post": {
        "summary": "Submit multiple jobs as a batch",
        "description": "Submit multiple jobs in a single request for batch processing.\n\nBounded by a 30,000-JOB cap and, separately, by the API-wide ~4.5 MB request cap. Which binds first depends on the tool. Jobs are counted AFTER design fan-out, so for a tool with a `numDesigns`-style field one row becomes many jobs and a small body can reach 30,000; for a tool without one a row is a job, and the size cap is usually what stops you, since 30,000 rows would have to average about 150 bytes each. Compare against the EXPANDED job count, not the row count.",
        "operationId": "submitBatch",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchSubmission"
              },
              "example": {
                "batchName": "my-batch-analysis",
                "type": "alphafold",
                "settings": [
                  {
                    "sequence": "QVQLQQSGAELARPGASVKMSCKASGYTFTRYTMHWVKQRPGQGLEWIGYINPSRGYTNYNQKFKDKATLTTDKSSSTAYMQLSSLTSEDSAVYYCARYYDDHYCLDYWGQGTTLTVSS"
                  },
                  {
                    "sequence": "MKTVRQERLKSIVRILERSKEPVSGAQLAEELSVSRQVIVQDIAYLRSLGYNIVATPRGYVLAGG"
                  }
                ],
                "jobNames": [
                  "job1",
                  "job2"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ordinary batches return a plain text body, `<batchName> batch submitted to queue.` Pipeline tools accept at most 500 settings rows with one submitId; they return PipelineToolSubmissionResponse without a parent. Omitted jobNames use batchName plus a zero-based row index. Retry the entire unchanged request after partial handoff.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "my-batch batch submitted to queue."
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PipelineToolSubmissionResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. TWO body shapes share this status code — see `/submit-job` for how to tell them apart (parse, do not read `Content-Type`) and for the flags a JSON rejection can carry. Most rejections here are a bare string.\n\nBatches of a finetuning tool are submitted with `POST /finetune-batch`. This route may refuse a batch whose `type` is a finetuning tool with JSON `{\"code\": \"use_finetune_endpoint\", \"endpoint\": \"/finetune-batch\"}` — resend the same body there with the tool in `model` instead of `type`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "Missing required field \"batchName\""
                }
              }
            }
          },
          "413": {
            "description": "Request body over the platform's ~4.5 MB cap. Generated at the edge, ahead of this endpoint, so the body is empty and NO jobs were created. Split the batch."
          },
          "422": {
            "description": "Pipeline-tool submission forwards validation failures from the Jobs API. The upstream response body and content type are preserved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "tags": [
          "Jobs"
        ]
      }
    },
    "/api/finetune": {
      "post": {
        "summary": "Submit a finetuning job",
        "description": "Train a model with a finetuning tool (the tools `GET /tools` and `GET /tools/{name}/schema` mark `finetune: true`, e.g. `chemprop-finetune`). The body is `/submit-job`'s with ONE difference: the tool is named by `model` instead of `type`. Every other field `/submit-job` accepts is accepted here with the same meaning, and auth, validation, spending checks and every response are `/submit-job`'s own — this route runs the same submit path.\n\nTo train the next VERSION of a model you have access to, set `settings.parentModelId` to its `id` (from `GET /finetuned-models`). Every finetuning tool whose worker records lineage accepts it; a tool that does not refuses it as an unknown setting, and a parent you cannot access, or one trained with a different tool, is refused with a plain-text 400. `proteus` is the notable exclusion: it registers no model at all, so there is nothing to version from.\n\nSubmit finetuning tools here rather than to `/submit-job` (and batches of them to `/finetune-batch` rather than `/submit-batch`): those may refuse them with 400 `use_finetune_endpoint`.",
        "operationId": "submitFinetune",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "`JobSubmission` with `model` in place of `type`. Declared inline rather than as a component because it differs from `JobSubmission` only in that one field.",
                "required": [
                  "jobName",
                  "model",
                  "settings"
                ],
                "additionalProperties": true,
                "properties": {
                  "jobName": {
                    "$ref": "#/components/schemas/JobSubmission/properties/jobName"
                  },
                  "model": {
                    "type": "string",
                    "description": "The finetuning tool to run — a tool listed with `finetune: true`. Exact and case-sensitive, like `type` on `/submit-job`.",
                    "example": "chemprop-finetune"
                  },
                  "type": {
                    "type": "string",
                    "description": "Do not send. Accepted only when it equals `model`; any other value is refused with `type_model_mismatch`."
                  },
                  "settings": {
                    "$ref": "#/components/schemas/JobSubmission/properties/settings"
                  },
                  "projectTag": {
                    "$ref": "#/components/schemas/ProjectTag"
                  }
                }
              },
              "example": {
                "jobName": "my-chemprop-model",
                "model": "chemprop-finetune",
                "settings": {
                  "task": "regression",
                  "csvFile": "solubility.csv",
                  "smilesColumn": "smiles",
                  "propertyColumn": "logS"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Identical to `/submit-job`: a PLAIN TEXT body, `<jobName> submitted to queue.`, carrying the normalized job name to poll.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "my-chemprop-model submitted to queue."
                }
              }
            }
          },
          "400": {
            "description": "Identical to `/submit-job` — TWO body shapes share this status, so parse the body and read what you got (see `/submit-job`). Where `/submit-job` checks `type`, this route checks `model` instead, refusing in this order with a JSON `ErrorResponse` whose `code` is one of: `model_required` (no `model`); `type_model_mismatch` (`type` was also sent and differs from `model`); `not_a_finetune_tool` (`model` is not a finetuning tool — submit it with `/submit-job`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "\"alphafold\" is not a finetuning tool. Submit it with POST /submit-job.",
                  "code": "not_a_finetune_tool"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "Missing required field \"jobName\""
                }
              }
            }
          }
        },
        "tags": [
          "Jobs"
        ]
      }
    },
    "/api/finetune-batch": {
      "post": {
        "summary": "Submit a batch of finetuning jobs",
        "description": "Train several models with one finetuning tool in a single request (the tools `GET /tools` and `GET /tools/{name}/schema` mark `finetune: true`). The body is `/submit-batch`'s with ONE difference: the tool is named by `model` instead of `type`. Every other field `/submit-batch` accepts is accepted here with the same meaning, and auth, validation, spending checks and every response are `/submit-batch`'s own — this route runs the same batch submit path.\n\n`settings[].parentModelId` versions a model per row, as `settings.parentModelId` does on `/finetune`, for the same set of tools.\n\nSubmit batches of finetuning tools here rather than to `/submit-batch`, which may refuse them with 400 `use_finetune_endpoint`.",
        "operationId": "submitFinetuneBatch",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "`BatchSubmission` with `model` in place of `type`. Declared inline rather than as a component because it differs from `BatchSubmission` only in that one field.",
                "required": [
                  "batchName",
                  "model",
                  "settings"
                ],
                "additionalProperties": true,
                "properties": {
                  "batchName": {
                    "$ref": "#/components/schemas/BatchSubmission/properties/batchName"
                  },
                  "model": {
                    "type": "string",
                    "description": "The finetuning tool to run for every job in the batch — a tool listed with `finetune: true`. Exact and case-sensitive, like `type` on `/submit-batch`.",
                    "example": "chemprop-finetune"
                  },
                  "type": {
                    "type": "string",
                    "description": "Do not send. Accepted only when it equals `model`; any other value is refused with `type_model_mismatch`."
                  },
                  "settings": {
                    "$ref": "#/components/schemas/BatchSubmission/properties/settings"
                  },
                  "jobNames": {
                    "$ref": "#/components/schemas/BatchSubmission/properties/jobNames"
                  },
                  "projectTag": {
                    "$ref": "#/components/schemas/ProjectTag"
                  }
                }
              },
              "example": {
                "batchName": "my-chemprop-models",
                "model": "chemprop-finetune",
                "settings": [
                  {
                    "task": "regression",
                    "csvFile": "solubility.csv",
                    "smilesColumn": "smiles",
                    "propertyColumn": "logS"
                  },
                  {
                    "task": "regression",
                    "csvFile": "permeability.csv",
                    "smilesColumn": "smiles",
                    "propertyColumn": "logPapp"
                  }
                ],
                "jobNames": [
                  "solubility-model",
                  "permeability-model"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A PLAIN TEXT body, `<batchName> batch submitted to queue.` — the sentence `/submit-batch` sends on success.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "my-property-models batch submitted to queue."
                }
              }
            }
          },
          "400": {
            "description": "Identical to `/submit-batch` — TWO body shapes share this status, so parse the body and read what you got (see `/submit-job`). Where `/submit-batch` checks `type`, this route checks `model` instead, refusing in this order with a JSON `ErrorResponse` whose `code` is one of: `model_required` (no `model`); `type_model_mismatch` (`type` was also sent and differs from `model`); `not_a_finetune_tool` (`model` is not a finetuning tool — submit it with `/submit-batch`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "\"alphafold\" is not a finetuning tool. Submit it with POST /submit-batch.",
                  "code": "not_a_finetune_tool"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "Missing required field \"batchName\""
                }
              }
            }
          }
        },
        "tags": [
          "Jobs"
        ]
      }
    },
    "/api/result": {
      "post": {
        "summary": "Get job results",
        "description": "Retrieve results for a completed job",
        "operationId": "getResult",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "jobName"
                ],
                "properties": {
                  "jobName": {
                    "type": "string",
                    "description": "Name of the job to get results for",
                    "example": "my-protein-analysis"
                  },
                  "jobEmail": {
                    "type": "string",
                    "format": "email",
                    "description": "Email of another member of your team (optional)",
                    "example": "user@email.com"
                  },
                  "fileName": {
                    "type": "string",
                    "description": "Path to a specific file in the job results (optional)",
                    "example": "myfile.txt"
                  },
                  "pdbsOnly": {
                    "type": "boolean",
                    "description": "Return only PDB files (optional)",
                    "example": true
                  },
                  "noAsync": {
                    "type": "boolean",
                    "description": "If true, fail with 400 instead of returning a 202 \"preparing\" response when the aggregated zip is not yet built. Use this if your client cannot poll. Default: false (the server falls back to an async build on miss).",
                    "example": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Job results. Returned when the aggregated zip already exists, or when the server was able to build it inline (short-ETA batches: the request is held open for up to ~290s while the batch-aggregate worker finishes, then the signed URL is returned).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "description": "S3 presigned URL to download the job results",
                  "example": "https://s3.amazonaws.com/bucket/job-results.zip"
                }
              }
            }
          },
          "202": {
            "description": "The aggregated result zip is not yet built and its estimated build time exceeds the inline-wait budget (~290s), or the wait budget elapsed before the zip was ready. The server has kicked off (or rejoined) a batch-aggregate worker that will build it on demand and upload it to the same S3 key /result will return. Wait, then repeat the same /result call.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "preparing"
                      ]
                    },
                    "jobName": {
                      "type": "string",
                      "description": "The batch parent's job name (echoed from the request)."
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable polling instructions."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request"
          }
        },
        "tags": [
          "Results"
        ]
      }
    },
    "/api/upload/{filename}": {
      "put": {
        "summary": "Upload a file",
        "description": "Upload a file (PDB, sequence, etc.) for use in job submissions",
        "operationId": "uploadFile",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "filename",
            "in": "path",
            "required": true,
            "description": "Name of the file to upload",
            "schema": {
              "type": "string",
              "example": "myfile.pdb"
            }
          },
          {
            "name": "folder",
            "in": "query",
            "description": "Optional folder to upload the file to",
            "schema": {
              "type": "string",
              "example": "myFolder"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary",
                "description": "File content to upload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "File uploaded successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Success message",
                      "example": "File uploaded successfully"
                    },
                    "fileUrl": {
                      "type": "string",
                      "description": "URL of the uploaded file"
                    },
                    "signedUrl": {
                      "type": "string",
                      "description": "Signed URL for accessing the file"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request - invalid file"
          },
          "413": {
            "description": "File too large"
          }
        },
        "tags": [
          "Files"
        ]
      }
    },
    "/api/delete-job": {
      "delete": {
        "summary": "Delete a job",
        "description": "Marks a job deleted and hides it from listings; for a batch, its subjobs too. This is a soft delete — result files in storage are not removed. An unknown job name returns 400.",
        "operationId": "deleteJob",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "jobName"
                ],
                "properties": {
                  "jobName": {
                    "type": "string",
                    "description": "Name of the job to delete",
                    "example": "myJobName"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Job deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Job deleted successfully"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request - job not found"
          }
        },
        "tags": [
          "Jobs"
        ]
      }
    },
    "/api/delete-file": {
      "delete": {
        "summary": "Delete a file",
        "description": "Delete a file from user account",
        "operationId": "deleteFile",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "filePath",
            "in": "query",
            "description": "Name of the file to delete",
            "schema": {
              "type": "string",
              "example": "path/to/myFileName.txt"
            }
          },
          {
            "name": "folder",
            "in": "query",
            "description": "Path to folder - deletes all files in the specified folder",
            "schema": {
              "type": "string",
              "example": "myFolder"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "File deleted successfully"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request - file not found"
          }
        },
        "tags": [
          "Files"
        ]
      }
    },
    "/api/files": {
      "get": {
        "summary": "Get user's files",
        "description": "Retrieve the complete array of file names for the selected folder. This endpoint does not paginate.",
        "operationId": "getFiles",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "includeFolders",
            "in": "query",
            "description": "Include folders in the response",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          },
          {
            "name": "folder",
            "in": "query",
            "description": "Path to folder to view files within that folder",
            "schema": {
              "type": "string",
              "example": "myFolder"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of files",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "tags": [
          "Files"
        ]
      }
    },
    "/api/agent/provision": {
      "post": {
        "summary": "Get an API key with no account — for AI agents",
        "description": "Mints a working API key with no authentication, no signup form, no email, no CAPTCHA and no payment method. POST with an empty body. Intended for an agent that has found this API and has no credential, including one acting for a user who is present. Not available on a dedicated deployment, where it returns 404.\n\nWhat comes back is a real free account with EVERY tool available and the FULL free monthly allowance — not a restricted sandbox and not a fraction of a tier. Do not substitute a simpler tool, or a smaller run, than the one you were asked for. `POST /api/validate-job` costs nothing and does NOT consume a job, so use it to get the payload right first. The response's `limits` is authoritative.\n\nThe one difference from a signed-up account is that an unclaimed credential EXPIRES (see `expiresAt`). `/api/submit-job` and `/api/submit-batch` both work; `/api/submit-pipeline`, `/api/run-pipeline` and `/api/virtual-screening` return `403` until a human claims it. That is this limit firing, not a malformed request, so do not retry with a different payload.\n\nThe response also carries a `claimUrl`. Give it to the person you are working for. One sign-in there MOVES the job you ran into that person's account so they can see it, and releases the rest of the monthly free tier. **The same API key keeps working**, so nothing already written against it has to change; afterwards it acts as that user. Unclaimed credentials stop working when `expiresAt` passes.\n\nStore the key you receive. Provisioning is rate-limited per source and platform-wide; asking for a second key because the first was not kept is the usual cause of a `429`. Full agent-authentication notes are at `/auth.md` on this host.",
        "operationId": "provisionAgentCredential",
        "security": [],
        "responses": {
          "201": {
            "description": "A trial credential was issued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apiKey": {
                      "type": "string",
                      "description": "Send as the `x-api-key` header on every subsequent request."
                    },
                    "email": {
                      "type": "string",
                      "description": "The synthetic identity this credential belongs to, on a pseudo-TLD that cannot be registered by anyone. It is not a mailbox and nothing is sent to it."
                    },
                    "tier": {
                      "type": "string",
                      "example": "agent-trial"
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "After this instant an unclaimed credential stops authenticating."
                    },
                    "limits": {
                      "type": "object",
                      "description": "Authoritative limits for THIS credential. Read these rather than assuming fixed values — they can change without a spec revision.",
                      "properties": {
                        "jobs": {
                          "type": "integer",
                          "description": "Monthly job allowance — the ordinary free tier.",
                          "example": 10
                        },
                        "allowedTools": {
                          "type": "string",
                          "description": "`\"all\"` — not tool-restricted. Present so a client does not have to infer it from the absence of a list.",
                          "example": "all"
                        },
                        "maxRuntimeSeconds": {
                          "type": "integer",
                          "nullable": true,
                          "description": "`null` — no agent-specific runtime cap; jobs are clamped by the ordinary free-tier rules."
                        },
                        "expiresInDays": {
                          "type": "integer",
                          "description": "How long the credential works if never claimed.",
                          "example": 30
                        }
                      }
                    },
                    "claimUrl": {
                      "type": "string",
                      "format": "uri",
                      "description": "Surface this to your human. One sign-in releases the rest of the free tier on the same account, without changing the API key."
                    },
                    "usage": {
                      "type": "object",
                      "description": "The endpoints to call next, on the host that issued this key."
                    },
                    "nextSteps": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "notice": {
                      "type": "string",
                      "description": "Plain-language statement of what this credential can and cannot do."
                    },
                    "terms": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Per-source or platform-wide provisioning limit reached. Reuse a key you already hold; otherwise honour `retryAfterSeconds` — do not retry in a loop — or have a human sign up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Provisioning is temporarily unavailable. The platform-wide counter fails CLOSED, so this is returned rather than issuing an uncounted credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools-catalog": {
      "get": {
        "summary": "List every tool name — no API key required",
        "description": "The one endpoint on this surface that needs no credentials, so an agent can confirm a tool name is real BEFORE the user has an account. Also served at `/tools.json` (and `/tools.md`) **on this same host** — this `/api/tools-catalog` path is the same handler and is documented here so it is reachable from the spec. Follow it on the host that served this spec: the catalog names submit and schema endpoints for whichever deployment answered, so fetching it from another host hands you that host's endpoints.\n\nReturns the exact, case-sensitive `type` string to send to `/submit-job`, plus each tool's REQUIRED settings. Guessing a name is the most common way generated code fails, and an unrecognised settings key is never rejected, only flagged — so a synonym surfaces as \"missing required field\", not as an unknown-key error.\n\nDeliberately NOT the whole schema: optional parameters, defaults and descriptions need a key (`/tools/{name}/schema`). Unlike `/tools`, this is not account-scoped — it lists what a brand-new public user could submit, so feature-flagged, domain-restricted and API-gated tools are omitted.",
        "operationId": "getPublicToolCatalog",
        "security": [],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "description": "Return only this tool, so a caller that already knows the name does not have to pull the whole catalogue into context.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tag",
            "in": "query",
            "description": "Return only tools carrying this intent tag (case-insensitive), e.g. `protein-ligand-docking`. The full tag list is in the `tags` field of every response.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "`md` returns the catalogue as a Markdown table (`text/markdown`) instead of JSON.",
            "schema": {
              "type": "string",
              "enum": [
                "md"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The public tool catalogue",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "$comment": {
                      "type": "string",
                      "description": "How to read `requiredSettings` — that requiredness is usually conditional, which key the `tasks` predicates refer to, and what the shape-bearing fields mean. Read it before treating the list as a checklist. Declared here because a typed generator exposes only declared properties, and dropping it drops the instructions."
                    },
                    "submitEndpoint": {
                      "type": "string",
                      "description": "The absolute submit URL, on the host this response was fetched from, with its auth requirement stated. Every route lives under `/api/`; `POST /submit-job` is a 404.",
                      "example": "POST https://<the-host-that-served-this>/api/submit-job (requires x-api-key)"
                    },
                    "schemaEndpoint": {
                      "type": "string",
                      "description": "The per-tool full-schema URL. Needs a key."
                    },
                    "validateEndpoint": {
                      "type": "string",
                      "description": "Dry-run a payload without spending a job. Needs a key too — free of compute, not free of auth."
                    },
                    "agentGuide": {
                      "type": "string",
                      "description": "The complete agent guide, on the host this was fetched from."
                    },
                    "openapi": {
                      "type": "string",
                      "description": "This document, on the host this response was fetched from."
                    },
                    "mcpServer": {
                      "type": "string",
                      "description": "MCP endpoint. Not host-derived — there is no per-tenant MCP.",
                      "example": "https://mcp.tamarind.bio/mcp"
                    },
                    "filter": {
                      "type": "object",
                      "description": "Present only when `type` or `tag` narrowed the response.",
                      "properties": {
                        "type": {
                          "type": "string"
                        },
                        "tag": {
                          "type": "string"
                        }
                      }
                    },
                    "ignoredParamCount": {
                      "type": "integer",
                      "description": "How many query parameters this endpoint did NOT understand. Present only when at least one was ignored, so a request using only `type`, `tag` and `format` is byte-identical to before this field existed. Anything counted here was DROPPED, not applied: `?search=vina` returns the whole catalogue with `filter` absent. Treat a non-zero value as \"my filter did not happen\" and re-issue with `?type=` (exact tool name) or `?tag=` (category). Declared here for the same reason as `$comment`: a typed generator exposes only declared properties, so an undeclared warning is a warning the caller cannot see. The ignored NAMES are deliberately not echoed back — this response is read by agents, and reflecting caller-supplied text into it would make the endpoint a prompt-injection relay for anyone who can choose the URL."
                    },
                    "ignoredParamsNote": {
                      "type": "string",
                      "description": "The same fact in prose, for an agent consuming this document as text rather than as a typed object. Present exactly when `ignoredParamCount` is."
                    },
                    "count": {
                      "type": "integer",
                      "description": "Tools in this response, after any `type`/`tag` filter"
                    },
                    "totalCount": {
                      "type": "integer",
                      "description": "Tools in the unfiltered catalogue"
                    },
                    "tags": {
                      "type": "array",
                      "description": "Every intent tag in use, for the `tag` parameter",
                      "items": {
                        "type": "string"
                      }
                    },
                    "tools": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PublicToolInfo"
                      }
                    }
                  }
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Returned when `format=md`"
                }
              }
            }
          }
        },
        "tags": [
          "Tools"
        ]
      }
    },
    "/api/tools": {
      "get": {
        "summary": "List available tools",
        "description": "The tools this account can DISCOVER, each with its settings schema. Scoped to the caller. Fetch this rather than assuming a tool name.\n\nAbsence does not prove a tool is unsubmittable: custom tools deployed on the current platform are runnable, and their schemas are available at `/tools/{name}/schema`, but they are not listed here.\n\nThe finetuning tools (`finetune: true`) are listed here like any other, and also on their own by `GET /models`.",
        "operationId": "listTools",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The available tools",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ToolInfo"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid API key"
          }
        },
        "tags": [
          "Tools"
        ]
      }
    },
    "/api/tools/{name}/schema": {
      "get": {
        "summary": "A tool's settings as JSON Schema",
        "description": "The parameters this tool accepts, as a standard JSON Schema document, scoped to your account. Validate a `settings` object against it before submitting. Domain types JSON Schema cannot express (a PDB file, a residue selection) travel as strings and keep their original type under `x-tamarind-type`.\n\nResolved through the same classifier `POST /submit-job` uses, so the schema describes the tool version your submission will actually run. A tool you cannot see and one that does not exist both answer 404.\n\nA custom tool that is mid-deploy (status `In Queue` or `Running`) also answers 404, because `/submit-job` refuses it in that state — the schema is unavailable until its deployment finishes rather than describing a contract you cannot submit.\n\nThis is a STATIC description, for code generation, form building and offline checking. To check a specific payload's FIELDS use `POST /validate-job`, which runs the same validator `/submit-job` does and so cannot disagree with it about field values. Note it validates fields only: it does not re-check org or team tool policy, or whether a custom tool is mid-deploy, so a job can still be refused at submission for those reasons.",
        "operationId": "getToolSchema",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "version",
            "in": "query",
            "description": "Describe a specific pinned build of a custom tool rather than the deployed one — the same `version` accepted by `POST /submit-job`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The tool's `name`, as returned by `GET /tools`.",
            "schema": {
              "type": "string",
              "example": "alphafold"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "JSON Schema for the tool's settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "properties": {
                    "finetune": {
                      "type": "boolean",
                      "description": "True for a finetuning tool, which is submitted with `POST /finetune` (tool in `model`) rather than `/submit-job`."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid API key. The classic surface answers 400 here rather than 401, and this endpoint follows it."
          },
          "404": {
            "description": "No such tool, or not available to this account — a tool you cannot run is reported the same way as one that does not exist."
          }
        },
        "tags": [
          "Tools"
        ]
      }
    },
    "/api/validate-job": {
      "post": {
        "summary": "Validate a job without submitting it",
        "description": "Runs the exact validation `/submit-job` runs, without submitting and at no cost. Returns 200 whether or not the payload is valid — read the `valid` field. On success `normalized` is the payload to submit, with defaults filled in.\n\n`settings` may also be an ARRAY, to pre-flight a whole batch in one call. Every row is validated — nothing is sampled — and the response carries one verdict per row in `results`, in input order, with `valid` as the aggregate. Prefer this over calling the endpoint once per job: a per-job loop over a large batch is a request volume that edge rate limiting refuses long before it reaches us. Up to 1000 rows (500 for pipeline tools) per call; split larger batches across calls.\n\nTwo SIZE limits bound a call, and with large payloads both bind before the row cap does. The REQUEST body is capped at ~4.5 MB by the platform, ahead of this endpoint: over it the caller gets a bare `413` with no JSON body at all, because nothing here ever runs. Split on BYTES as well as rows — 1000 rows is only reachable at roughly 4 KB a row.\n\nThe verdicts also have to fit in one response. Since each row echoes its `normalized` settings with defaults filled in, a batch of very large payloads can exceed that even when the request itself was accepted; it comes back as an ordinary `valid: false` naming the limit, and the fix is to validate in smaller calls. Pipeline-tool preflight checks settings; Jobs admission checks pipeline quota, budgets and molecule ownership at submission. A valid verdict does not reserve admission.",
        "operationId": "validateJob",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "The tool is named by `type`, or by `model` — so the body you would send to `/submit-job` or to `/finetune` validates as-is. One of the two is required.",
                "required": [
                  "settings"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "example": "esmfold"
                  },
                  "model": {
                    "type": "string",
                    "description": "Alias for `type`, used when `type` is absent (the `/finetune` body shape). Sending both with different values is an invalid verdict with `code` `type_model_mismatch`."
                  },
                  "settings": {
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/JobSubmission/properties/settings"
                      },
                      {
                        "type": "array",
                        "description": "Batch mode — one settings object per job, same shape `/submit-batch` takes. Max 1000 rows per call, or 500 for pipeline tools.",
                        "minItems": 1,
                        "maxItems": 1000,
                        "items": {
                          "$ref": "#/components/schemas/JobSubmission/properties/settings"
                        }
                      }
                    ]
                  },
                  "inputs": {
                    "description": "Pipeline-tool typed inputs; a single input list or one list per settings row.",
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/JobSubmission/properties/inputs"
                      },
                      {
                        "$ref": "#/components/schemas/BatchSubmission/properties/inputs"
                      }
                    ]
                  },
                  "jobName": {
                    "type": "string",
                    "description": "Optional, single-payload mode only — when given, a duplicate name is reported as invalid."
                  },
                  "jobNames": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Optional, array mode only. Same length as `settings`. A name repeated within the array is reported as invalid, matching `/submit-batch`. A name that collides with an EXISTING job is not: submission namespaces the batch under its `batchName` instead of refusing, so no `job_name` is echoed per row. Pipeline tools check each supplied row name for existing-job conflicts."
                  },
                  "version": {
                    "type": "string",
                    "description": "Optional. Pin validation to a specific custom-tool build (the same `version` you passed to `/tools/{name}/schema`). Omit it and validation resolves the deployed version, which may declare a different set of settings than the build you fetched the schema for."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The verdict. Note that an invalid payload is also a 200.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationResult"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid API key"
          },
          "405": {
            "description": "Method not allowed"
          },
          "413": {
            "description": "Request body over the API-wide ~4.5 MB cap. Generated ahead of this endpoint, so the body is empty — there is no `valid` field to read. For an array, split it into smaller calls. For a single payload the cause is almost always inline file content: upload the file and reference it by path instead."
          }
        },
        "tags": [
          "Jobs"
        ]
      }
    },
    "/api/estimate-job": {
      "post": {
        "summary": "Estimate a job's runtime and compute cost without submitting it",
        "description": "Returns the SAME runtime + weighted-hours estimate the `/submit-job` budget gate computes, without submitting — read-only, costs nothing, consumes no quota. Use it to plan a run, compare tools, or budget a design loop BEFORE spending compute.\n\n`settings` may also be an ARRAY, to quote a whole batch in one call (same shape `/validate-job` takes). Every row is priced and the response carries one quote per row in `results`, in input order, plus `numJobs` and `computeHours` totals. A row that cannot be priced carries its own `error` in place instead of failing the call. Up to 30,000 rows per call (the `/submit-batch` job ceiling); the binding limit for large rows is the ~4.5 MB request-body cap, enforced ahead of this endpoint — split on BYTES as well as rows.\n\n`computeHours` (the compute-hour billing unit, the number the submit budget gate checks) is returned to Premium users only, mirroring the website; free-tier callers get the runtime estimate plus a `note`. The estimate is ADVISORY: a transient failure answers `503` with `retriable: true` rather than a quote, and submission never requires one.",
        "operationId": "estimateJob",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type",
                  "settings"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "example": "esmfold"
                  },
                  "settings": {
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/JobSubmission/properties/settings"
                      },
                      {
                        "type": "array",
                        "description": "Batch mode — one settings object per job, same shape `/submit-batch` takes. Max 30,000 rows per call.",
                        "minItems": 1,
                        "maxItems": 30000,
                        "items": {
                          "$ref": "#/components/schemas/JobSubmission/properties/settings"
                        }
                      }
                    ]
                  },
                  "jobName": {
                    "type": "string",
                    "description": "Optional."
                  },
                  "version": {
                    "type": "string",
                    "description": "Optional. Pin the quote to a specific custom-tool build (the same `version` you would pass to `/tools/{name}/schema` and `/submit-job`)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The quote. Single-payload mode returns one estimate; array mode returns per-row `results` plus totals.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobType": {
                      "type": "string"
                    },
                    "numJobs": {
                      "type": "integer",
                      "description": "How many jobs would be submitted (totalled across rows in array mode) — e.g. multiple sequences fan out."
                    },
                    "runtime": {
                      "type": "integer",
                      "description": "Wall-clock estimate for ONE job, in seconds."
                    },
                    "computeHours": {
                      "type": "number",
                      "description": "Total weighted (compute) hours across the whole submission — the billing unit the submit budget gate checks. Premium callers only."
                    },
                    "note": {
                      "type": "string",
                      "description": "Present instead of `computeHours` for free-tier callers."
                    },
                    "count": {
                      "type": "integer",
                      "description": "Array mode — how many rows were sent."
                    },
                    "estimated_count": {
                      "type": "integer",
                      "description": "Array mode — how many rows quoted successfully."
                    },
                    "results": {
                      "type": "array",
                      "description": "Array mode — one entry per input index, in input order. Each carries the same per-job fields as single-payload mode, or an `error` when that row could not be priced.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "index": {
                            "type": "integer"
                          },
                          "numJobs": {
                            "type": "integer"
                          },
                          "runtime": {
                            "type": "integer"
                          },
                          "computeHours": {
                            "type": "number"
                          },
                          "error": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid API key, unknown or unsupported tool, a malformed `settings` payload, or a row count over the 30,000-row cap."
          },
          "403": {
            "description": "The caller's organization has denied this tool, or estimates are not enabled for this account (`featureDisabled: true`)."
          },
          "405": {
            "description": "Method not allowed"
          },
          "413": {
            "description": "Request body over the API-wide ~4.5 MB cap; split the batch."
          },
          "422": {
            "description": "A finite estimate could not be computed for these settings."
          },
          "503": {
            "description": "Transient failure; the body carries `retriable: true`. The estimate is advisory — retry, or proceed without it."
          }
        },
        "tags": [
          "Jobs"
        ]
      }
    },
    "/api/stop-job": {
      "post": {
        "summary": "Stop a running or queued job",
        "description": "Stops a job that is Running, In Queue, Pending or Waiting. For a batch, stops every stoppable child and the parent.",
        "operationId": "stopJob",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "jobName"
                ],
                "properties": {
                  "jobName": {
                    "type": "string",
                    "example": "my-protein-analysis"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stopped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "stoppedCount": {
                      "type": "integer",
                      "description": "How many jobs were stopped, including batch children."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid API key, unknown job, or the job is not in a stoppable state"
          },
          "405": {
            "description": "Method not allowed"
          }
        },
        "tags": [
          "Jobs"
        ]
      }
    },
    "/api/models": {
      "get": {
        "summary": "List the models you can finetune",
        "description": "The finetuning subset of `GET /tools`: the tools this account can train a model with, i.e. exactly the `/tools` entries with `finetune: true`, and every one of them. It is `/tools` run with a filter, so auth, query parameters, gating (feature flags, tier, organization restrictions) and each item are `/tools`' own: an item here is byte-for-byte the `/tools` item for the same tool and caller, so `finetune` is always `true`. Finetuning tools are NOT removed from `/tools`. `custom=true` behaves as on `/tools` (custom tools are not listed by either), so it returns an empty array.\n\nTrain one with `POST /finetune` (or `POST /finetune-batch`), naming the item's `name` in `model`. This is NOT `GET /finetuned-models`, which lists the models you have already TRAINED (and runs them by their `inferenceType`); `/models` lists which tools you can train them WITH.",
        "operationId": "listFinetunableModels",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The finetuning tools available to this account",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ToolInfo"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid API key"
          }
        },
        "tags": [
          "Models"
        ]
      }
    },
    "/api/finetuned-models": {
      "get": {
        "summary": "List your finetuned models",
        "description": "Models you own (`personalModels`), plus those shared within your organization (`organizationModels`). To run one, submit its `inferenceType` to `POST /submit-job` with its `id` as `modelId`. Only a model whose `status` is `Available` can be run. A model in `organizationModels` can be run while its owner shares it and your organization has sharing between members turned on. A model runs only on its own `inferenceType`; any other `type` is refused. The tools you can train a model WITH are `GET /models`.",
        "operationId": "listFinetunedModels",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "description": "Filter by finetune type, e.g. plm-finetune.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The available models",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "personalModels": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FinetunedModel"
                      }
                    },
                    "organizationModels": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FinetunedModel"
                      }
                    },
                    "totalCount": {
                      "type": "integer"
                    },
                    "personalTruncated": {
                      "type": "boolean",
                      "description": "True when `personalModels` stopped at `limit` with more of your models unread. The limit is a cap, not a page size."
                    },
                    "organizationTruncated": {
                      "type": "boolean",
                      "description": "True when `organizationModels` may be missing models past `limit`. Raise `limit` before concluding a teammate's model does not exist (`type` filters the rows already read, so it cannot reach past it)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid type or limit"
          },
          "401": {
            "description": "Missing or invalid credentials"
          }
        },
        "tags": [
          "Models"
        ]
      }
    },
    "/api/usage-statistics": {
      "get": {
        "summary": "Usage statistics",
        "description": "Weighted-hours, hours, or job counts. Organization scope is the default and covers every member; if the caller is not authorized for it, the request is served at user scope instead — read `metadata.scope` to see which was applied.",
        "operationId": "getUsageStatistics",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "statistic",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "hours",
                "weighted_hours",
                "jobs"
              ],
              "default": "hours"
            }
          },
          {
            "name": "scope",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "user",
                "organization"
              ],
              "default": "organization"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Usage, one entry per member in scope",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "users": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "email": {
                            "type": "string"
                          },
                          "total": {
                            "type": "number"
                          },
                          "tools": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "number"
                            }
                          }
                        }
                      }
                    },
                    "lastUpdated": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "metadata": {
                      "type": "object",
                      "properties": {
                        "statistic": {
                          "type": "string"
                        },
                        "scope": {
                          "type": "string",
                          "description": "The scope actually applied, which may be narrower than requested."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials"
          },
          "403": {
            "description": "Organization membership could not be verified"
          }
        },
        "tags": [
          "Usage"
        ]
      }
    },
    "/api/submit-pipeline": {
      "post": {
        "summary": "Create and run a new pipeline",
        "description": "Defines a multi-stage pipeline inline and submits it. Each stage names a task and the tools to run for it; a stage's outputs feed the next. This is the legacy pipeline API — new integrations should use the pipelines endpoints under `/api/pipelines`, which separate a reusable template from a run.",
        "operationId": "submitPipeline",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "jobName",
                  "stages"
                ],
                "properties": {
                  "jobName": {
                    "type": "string",
                    "description": "Name for this pipeline, unique within your account."
                  },
                  "stages": {
                    "type": "array",
                    "minItems": 1,
                    "description": "The stages to run, in order.",
                    "items": {
                      "$ref": "#/components/schemas/PipelineStage"
                    }
                  },
                  "initialInputs": {
                    "type": "array",
                    "minItems": 1,
                    "description": "Inputs fed into the first stage. Required (non-empty) whenever any first-stage setting has the value `\"pipe\"`, which marks the field that each initial input is substituted into; omitting it then is a 400 `Missing initial inputs`. Each entry is a raw sequence, or the name of a file you uploaded — `.pdb`/`.sdf` are passed through as file inputs, and a `.fa`/`.fasta` is expanded server-side into its sequences.",
                    "items": {
                      "type": "string"
                    }
                  },
                  "projectTag": {
                    "$ref": "#/components/schemas/ProjectTag"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submitted. Body is the plain-text confirmation `Pipeline {jobName} submitted to queue.`",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "A missing/empty `stages`, a duplicate `jobName`, a stage with no task or tools, an unknown filter metric, or an unsupported tool — each a bare string, the message in full. An API-key rejection and the required-project refusal are JSON instead. See `/submit-job` for how to tell the two shapes apart — parse the body, do not read `Content-Type` — and for the flags a JSON rejection carries.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "Missing initial inputs"
                }
              }
            }
          },
          "403": {
            "description": "A tool in the pipeline is not available to your account"
          },
          "503": {
            "description": "A tool could not be resolved (undeployed, or a transient error) — retry"
          }
        },
        "tags": [
          "Pipelines"
        ]
      }
    },
    "/api/projects/list": {
      "get": {
        "summary": "List your projects",
        "description": "Every project you can tag a job with: your organization's projects, your own personal ones, and any private project you have been invited to.\nRead this to resolve a `projectTag`. Only ORGANIZATION projects (`scope: \"org\"`) satisfy an org's \"require a project tag\" policy — a personal or shared project does not, and submitting with one is refused exactly as if you had sent none.",
        "operationId": "listProjects",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Your visible projects.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "projects": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Project"
                      }
                    },
                    "isInOrg": {
                      "type": "boolean",
                      "description": "Whether you belong to an organization."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/projects/create": {
      "post": {
        "summary": "Create a project",
        "description": "Create a project you can then pass as `projectTag`. Creates it under your ORGANIZATION by default, which is the scope that satisfies a \"require a project tag\" policy; pass `scope: \"personal\"` for a private one, which does not.\nCreating grants no privilege an interactive user does not already have — the key acts as you. If your organization curates its project list, prefer `GET /projects/list` and reuse an existing one rather than creating a near-duplicate.",
        "operationId": "createProject",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Project name, unique within the scope it is created in."
                  },
                  "description": {
                    "type": "string"
                  },
                  "color": {
                    "type": "string",
                    "description": "Hex colour for the UI. Omit to be assigned one at random."
                  },
                  "scope": {
                    "type": "string",
                    "enum": [
                      "org",
                      "personal"
                    ],
                    "description": "Defaults to `org`. `personal` is only honoured if you belong to an organization; otherwise every project is personal anyway."
                  },
                  "members": {
                    "type": "array",
                    "description": "Emails to invite. Only applies to a `personal`-scoped project — an org project is already visible to the whole org. Addresses outside your organization are dropped silently.",
                    "items": {
                      "type": "string",
                      "format": "email"
                    }
                  }
                }
              },
              "example": {
                "name": "Programme A",
                "description": "Q3 binder campaign"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "project": {
                      "$ref": "#/components/schemas/Project"
                    },
                    "members": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "failedAdds": {
                      "type": "array",
                      "description": "Members that could not be invited. The project was still created.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid name, colour or description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "A project with this name already exists in that scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/updateJobProject": {
      "put": {
        "summary": "Move a job to a different project",
        "description": "Reassign a job to another project, or clear its project by sending `projectId: null`. The job keeps its outputs and can be moved back, so this is the way to correct a filing decision rather than re-running the work.\nSend the PARENT job of a batch. Child jobs inherit their parent's project and are refused with 403 on their own.\nThe same rules the app enforces apply here, and they are anchored on the caller's identity rather than on how they authenticated. A job that currently sits in someone else's shared project, and a move INTO one, both require you to be the job's owner. An organization that requires project tagging will refuse a move that clears the project or sends the job to a personal one.",
        "operationId": "updateJobProject",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "oneOf": [
                  {
                    "required": [
                      "jobName"
                    ]
                  },
                  {
                    "required": [
                      "jobId"
                    ]
                  }
                ],
                "properties": {
                  "jobName": {
                    "type": "string",
                    "description": "The job's name, as returned by `POST /submit-job` or `GET /jobs`. Only your own jobs. Send either jobName or jobId, not both."
                  },
                  "jobId": {
                    "type": "string",
                    "description": "For a pipeline run, its `pipelineRunId` from `GET /jobs?projectId=`. Send either jobName or jobId, not both."
                  },
                  "projectId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Target project ID, as returned by `GET /projects/list`. Send null or omit to remove the job from its project."
                  }
                }
              },
              "example": {
                "jobName": "my-alphafold-run",
                "projectId": "proj_1730000000000_d4e5f6"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Moved. A reassignment echoes the project the job now sits in; a CLEAR (`projectId` null or omitted) has no project to name, so the field is absent rather than null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "project": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Project"
                        }
                      ],
                      "description": "The project the job was moved into. ABSENT on the clear path."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Neither `jobName` nor `jobId` sent, both sent, or an empty / non-string `jobName` (send exactly one); or the job owner's organization has projects DISABLED and this request supplies a `projectId` — assignment is refused there, while CLEARING (omitting `projectId`) stays allowed, because taking a job out of a project is the direction that setting points and refusing it would strand jobs tagged before the switch was thrown; or the move is refused by the organization's require-a-project policy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "A child job, or a job whose current or target shared project you do not own.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such job, or no such project in your scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/projects/update": {
      "put": {
        "summary": "Update a project",
        "description": "Rename a project, recolour it, change its description, and change who it is shared with. Jobs reference a project by ID, so a rename keeps every job already filed under it — which is why renaming is the right way to correct a name, rather than creating a second project and splitting the history.\nOnly the project's CREATOR may change it; org membership alone is not enough. That rule is the boundary here, and it is the same one whether you authenticate with a session or an API key: every field below is available to both.\nMembership (`addMembers`, `removeMembers`) applies to a PERSONAL-scoped project only — an organization project is already visible to the whole organization, so member changes on one are accepted and silently do nothing. Read the result back with `GET /projects/members/list`.",
        "operationId": "updateProject",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "projectId"
                ],
                "properties": {
                  "projectId": {
                    "type": "string",
                    "description": "The project's ID, as returned by `GET /projects/list`."
                  },
                  "name": {
                    "type": "string",
                    "description": "New name, unique within the scope the project lives in."
                  },
                  "description": {
                    "type": "string"
                  },
                  "color": {
                    "type": "string",
                    "description": "Hex colour for the UI."
                  },
                  "addMembers": {
                    "type": "array",
                    "description": "Emails to invite, on a `personal`-scoped project. Addresses outside your organization, and your own, are dropped silently. Must be an array if present.",
                    "items": {
                      "type": "string",
                      "format": "email"
                    }
                  },
                  "removeMembers": {
                    "type": "array",
                    "description": "Emails to revoke, on a `personal`-scoped project. Must be an array if present.",
                    "items": {
                      "type": "string",
                      "format": "email"
                    }
                  }
                }
              },
              "example": {
                "projectId": "proj_1730000000000_a1b2c3",
                "name": "Programme B"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "project": {
                      "$ref": "#/components/schemas/Project"
                    },
                    "failedAdds": {
                      "type": "array",
                      "description": "Present only when an invitation could not be written. Every other change in the request still applied.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `projectId`; an invalid name, colour or description; or an `addMembers` / `removeMembers` that is present but not an array. The member shape is checked for EVERY caller, before anything is written, and the check runs ahead of the update so a malformed list cannot leave a rename committed behind a rejection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "You did not create this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such project in your scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "A project with this name already exists in that scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "The delete failed. If the body carries `partiallyApplied: true`, it failed PART WAY: `appliedJobCount` jobs were already moved or unfiled and `remainingJobCount` were not, and the project was KEPT holding the rest. There is no transaction over an unbounded number of rows, so this state is reachable and is not the same as nothing having happened. Retry the SAME request to finish — both writes are idempotent and the rows that succeeded no longer carry this project's tag, so the retry picks up exactly what is left. Do NOT switch between `moveToProjectId` and `confirmJobRemoval` on the retry: that splits the project's jobs across two outcomes with nothing afterwards able to say which went which way. Without `partiallyApplied`, nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "partiallyApplied": {
                          "type": "boolean"
                        },
                        "appliedJobCount": {
                          "type": "integer"
                        },
                        "remainingJobCount": {
                          "type": "integer"
                        },
                        "movedToProjectId": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/projects/delete": {
      "delete": {
        "summary": "Delete a project",
        "description": "Delete a project. An EMPTY project needs only `projectId`. A project that still holds jobs needs you to say what happens to them, because the delete takes them out of it either way — send `moveToProjectId` to refile them under another project, or `confirmJobRemoval: true` to leave them in no project. Send neither and the call is REFUSED with 400 and nothing is written; the body carries `jobCount` so you can decide. No job is EVER deleted here, whichever you choose.\nDeleting once unfiled the jobs unconditionally, which for an ORGANIZATION project meant jobs your colleagues had filed here as well as your own, with no undo and no record of what any of them had been filed under — the 200 looked identical to an empty project's. It was then refused outright, which left you to do the same sweep by hand through `PUT /updateJobProject`, one job at a time. Both directions are now available in one call, and neither happens unless you asked for it.\nIf your organization requires every job to carry a project, `confirmJobRemoval` is not available to you — a job with no project is a state that policy refuses — so `moveToProjectId` is required, and must name an ORGANIZATION project. A personal project does not satisfy the policy.\nIf your organization has projects DISABLED, `moveToProjectId` is refused: a disabled organization may take jobs OUT of projects but not file them into one, the same rule `PUT /updateJobProject` applies. `confirmJobRemoval` still works, so a legacy project can still be deleted.\nA bulk move only ever targets an organization project, or your own personal project when the project being deleted is also your own personal one. Anything else (a project shared with you, or a personal project while the one being deleted is an org project) is 403: those moves need the per-job ownership check that `PUT /updateJobProject` does row by row.\nOnly the project's CREATOR may delete it; org membership alone is not enough.\nAN API KEY SWEEPS ONLY ITS OWN JOBS. Deleting an organization project that holds colleagues' jobs moves or unfiles those jobs too, and that is a web-app operation: over an API key this call is refused with 403 if the project holds any job belonging to someone else, the same rule `PUT /updateJobProject` applies one job at a time. Move those jobs individually with that endpoint, which checks ownership per job, or delete the project from the web app.\nWhat the call does destroy is the project row, its notes and its invitations, and that cannot be undone: creating a project with the same name again re-files nothing.\n`projectId` goes in a JSON body. A client that strips bodies from DELETE requests sends an empty one and gets the 400 for a missing `projectId`.",
        "operationId": "deleteProject",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "projectId"
                ],
                "properties": {
                  "projectId": {
                    "type": "string",
                    "description": "The project's ID, as returned by `GET /projects/list`."
                  },
                  "moveToProjectId": {
                    "type": "string",
                    "description": "Refile this project's jobs under the project with this ID instead of leaving them in none, then delete this project. Required (rather than optional) when your organization requires every job to carry a project, and must then be an ORGANIZATION project. Ignored when the project is already empty. 404 if it does not resolve in your scope, 400 if it is archived or is the project being deleted, 403 if a bulk move cannot legally reach it."
                  },
                  "confirmJobRemoval": {
                    "type": "boolean",
                    "description": "`true` acknowledges that the project's jobs are to be left in NO project, and is what permits the delete to proceed without `moveToProjectId`. Ignored when the project is already empty, and refused outright when your organization requires every job to carry a project. The jobs keep their results and settings; they simply no longer carry a project, and can be refiled from the jobs list or with `PUT /updateJobProject`."
                  }
                }
              },
              "example": {
                "projectId": "proj_1730000000000_a1b2c3",
                "moveToProjectId": "proj_1730000000001_d4e5f6"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deleted. `movedJobCount` and `unassignedJobCount` say which way its jobs went — at most one of them is ever non-zero, and both are 0 when the project was empty.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "unassignedJobCount": {
                      "type": "integer",
                      "description": "How many jobs you can see in Results are now in NO project because of this delete. That is what you asked for when you sent `confirmJobRemoval`; on a delete of an empty project a non-zero value instead means a job was filed under the project between the count and the delete and lost its project to it — the handler does not close that race. Always 0 when `moveToProjectId` was used. Rows Results never shows (deleted jobs, batch children, internal rows) are also swept but are not counted here. An unassigned job still exists and still has its results; it simply no longer carries a project. No job is ever deleted here."
                    },
                    "movedJobCount": {
                      "type": "integer",
                      "description": "How many jobs you can see in Results were refiled under `moveToProjectId`. 0 unless that was sent. Counted the same way as `unassignedJobCount`, so rows Results never shows are moved along with their jobs without appearing in this number."
                    },
                    "movedToProjectId": {
                      "type": "string",
                      "nullable": true,
                      "description": "The project the jobs were refiled under, echoed back, or `null` if none were."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `projectId`; or the project still holds jobs and the request said nothing about what should happen to them. That body carries `jobCount` beside `error`, and nothing is written. Retry with `moveToProjectId` to refile them, or `confirmJobRemoval: true` to leave them in no project; `GET /projects/list` returns the projects you can move to. The body also carries `requiresConfirmation: true`, or `requiresMoveTarget: true` when your organization requires every job to carry a project and only the move is available. Also 400 for a `moveToProjectId` that is archived or is the project being deleted, for one that does not satisfy a required-tag policy (a personal project does not), and for any `moveToProjectId` at all when your organization has projects disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "jobCount": {
                          "type": "integer",
                          "description": "How many jobs are still filed under the project, present only on the non-empty refusal. Composed onto `ErrorResponse` rather than added to it, because this is the only error that carries a count and the shared shape should not grow a field every other error omits. A generated client needs it declared here: the description above tells callers to act on this number, and without the declaration it is dropped on exactly the recovery path that needs it."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "You did not create this project; or a bulk move cannot legally reach the target you named; or you are calling with an API key and the project holds jobs belonging to someone else, which `foreignJobCount` counts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such project in your scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/projects/members/list": {
      "get": {
        "summary": "List a project's members",
        "description": "Who has been invited to a project. Read this to confirm an invitation actually landed, and to see the current list before changing it with `PUT /projects/update` — a membership change made through the API is readable back here, by the same identity that made it.\nMembers exist only on a PERSONAL-scoped project. An organization project is visible to the whole organization by definition and carries no member rows at all, so it answers an empty list rather than naming your colleagues.\nYou must already be able to reach the project: you created it (your organization's projects and your own personal ones), or you hold an invitation to it. Anything else is a 404, including a project id that exists but is not yours to see.\nAn invitation is re-checked on read rather than trusted. A member who has since left the project creator's organization no longer resolves, so a known project id does not keep returning a roster after the invitation has stopped meaning anything.",
        "operationId": "listProjectMembers",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "projectId",
            "in": "query",
            "required": true,
            "description": "The project's ID, as returned by `GET /projects/list`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The project's members. An organization project, and a personal project nobody has been invited to, both answer an empty list — this response does not distinguish them.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "email": {
                            "type": "string",
                            "format": "email",
                            "description": "The invited member, stored lowercased. Send this back as `removeMembers` on `PUT /projects/update`."
                          },
                          "invitedBy": {
                            "type": "string",
                            "format": "email",
                            "description": "Who issued the invitation."
                          },
                          "invitedAt": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the invitation was written, ISO 8601."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `projectId`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such project, or you neither created it nor hold an invitation to it. The two are deliberately the same answer: a project you cannot reach must not be distinguishable from one that does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/projects/members/leave": {
      "post": {
        "summary": "Leave a project you were invited to",
        "description": "Remove YOURSELF from a project someone else shared with you. This is the one project call that names nobody but the caller: the only field is the project, so it can never remove another person, and it is the half of membership you can exercise on a project you did not create. Removing someone ELSE is `removeMembers` on `PUT /projects/update`, and only the project's creator can do that.\nAfterwards the project stops appearing in `GET /projects/list` and you can no longer read it. Getting back in needs a fresh invitation from the project's creator — there is no rejoin call — so confirm with the user before leaving on their behalf.\nNothing else is touched. Jobs you filed under the project keep their results and keep their project tag; the project itself, its other members and its creator are unaffected.",
        "operationId": "leaveProject",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "projectId"
                ],
                "properties": {
                  "projectId": {
                    "type": "string",
                    "description": "The project's ID, as returned by `GET /projects/list`."
                  }
                }
              },
              "example": {
                "projectId": "proj_1730000000000_a1b2c3"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "You are no longer a member of this project.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `projectId`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "You are not a member of this project — which is an ORDINARY outcome, not a fault, and never worth retrying: the request was understood and there was nothing to remove. It is the answer for a project you already left, for one you never belonged to, for a project that does not exist, and for a project you CREATED (a creator owns their project rather than holding a membership row in it, so leaving is not the call — `DELETE /projects/delete` is). Treat it as \"you are not in this project\", which is the state you were asking for.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/run-pipeline": {
      "post": {
        "summary": "Run a saved pipeline",
        "description": "Runs a saved multi-stage pipeline by name. This is the legacy pipeline API; new integrations should use the pipelines endpoints under `/api/pipelines`.",
        "operationId": "runPipeline",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "jobName",
                  "pipelineName",
                  "initialInputs"
                ],
                "properties": {
                  "jobName": {
                    "type": "string",
                    "description": "Name for this pipeline execution."
                  },
                  "pipelineName": {
                    "type": "string"
                  },
                  "version": {
                    "type": "string",
                    "description": "Optional saved version; defaults to the pipeline's default."
                  },
                  "initialInputs": {
                    "type": "array",
                    "minItems": 1,
                    "description": "Uploaded .pdb filenames or raw sequences, matching the pipeline's configured input type. Must be non-empty. Basenames must be unique — child job names are derived from them.",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submitted",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Pipeline \"my-pipeline\" execution \"run-01\" submitted to queue with 3 jobs."
              }
            }
          },
          "400": {
            "description": "Missing or invalid API key, or invalid inputs"
          },
          "403": {
            "description": "Denied — a tool in the pipeline is restricted for this account, or a budget cap would be exceeded."
          },
          "404": {
            "description": "Pipeline not found"
          }
        },
        "tags": [
          "Pipelines"
        ]
      }
    },
    "/api/model-router": {
      "post": {
        "summary": "Recommend a tool for a task",
        "description": "Recommend which model to run, given your inputs and natural language prompt, backed by Tamarind benchmarks. Currently supports structure prediction, more tasks coming soon.",
        "operationId": "recommendTools",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicToolRecommendationRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The recommendation. An abstention is also a 200.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicToolRecommendation"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassicPublicProblem"
                }
              }
            }
          },
          "403": {
            "description": "Tool recommendations are not enabled for this account.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassicPublicProblem"
                }
              }
            }
          },
          "413": {
            "description": "Request body is too large.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassicPublicProblem"
                }
              }
            }
          },
          "422": {
            "description": "One or more request fields are invalid.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassicPublicProblem"
                }
              }
            }
          },
          "502": {
            "description": "The recommendation service rejected an upstream request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassicPublicProblem"
                }
              }
            }
          },
          "503": {
            "description": "Tool recommendations are not available in this environment.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassicPublicProblem"
                }
              }
            }
          }
        },
        "tags": [
          "Tools"
        ]
      }
    },
    "/api/assay-types": {
      "get": {
        "summary": "List assay types",
        "description": "Lists the lab assays you can order, such as binding affinity or expression.\n\nEach assay type lists its `settings`, in the same format `GET /tools` uses for a tool's\nsettings: a name, a type, whether it is required, and the `options` to choose from\nwhen there is a fixed set. An order is for one assay type.",
        "operationId": "listAssayTypes",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "provider",
            "in": "query",
            "description": "Only list assay types from this lab.",
            "schema": {
              "type": "string",
              "enum": [
                "adaptyv",
                "ginkgo",
                "aalpha"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The assay types you can order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "assayTypes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "assayType": {
                            "type": "string",
                            "example": "adaptyv_affinity"
                          },
                          "name": {
                            "type": "string"
                          },
                          "provider": {
                            "type": "string"
                          },
                          "providerName": {
                            "type": "string"
                          },
                          "category": {
                            "type": "string"
                          },
                          "method": {
                            "type": "string",
                            "nullable": true
                          },
                          "description": {
                            "type": "string"
                          },
                          "details": {
                            "type": "string",
                            "nullable": true
                          },
                          "modalities": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "enum": [
                                "antibody",
                                "protein"
                              ]
                            }
                          },
                          "multiChainProtein": {
                            "type": "boolean",
                            "description": "Whether a protein can have several chains, joined with \":\"."
                          },
                          "minSequences": {
                            "type": "integer"
                          },
                          "maxSequences": {
                            "type": "integer"
                          },
                          "turnaround": {
                            "type": "string",
                            "nullable": true
                          },
                          "includesAssays": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string"
                                },
                                "name": {
                                  "type": "string"
                                }
                              }
                            }
                          },
                          "targetsProvider": {
                            "type": "string",
                            "nullable": true,
                            "description": "Set when this assay needs a target. Look targets up with GET /assay-targets, using this value as `provider`."
                          },
                          "settings": {
                            "type": "array",
                            "description": "The settings this assay takes. Empty when it takes none.",
                            "items": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "type": {
                                  "type": "string",
                                  "description": "`string`, `number`, or `dropdown` when the value is one of `options`."
                                },
                                "required": {
                                  "type": "boolean"
                                },
                                "description": {
                                  "type": "string"
                                },
                                "options": {
                                  "type": "array",
                                  "items": {},
                                  "description": "The values you can choose from."
                                },
                                "default": {
                                  "description": "The value used when you leave the setting out."
                                },
                                "list": {
                                  "type": "boolean",
                                  "description": "True when the setting takes an array of `type`."
                                }
                              }
                            }
                          },
                          "docUrl": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "wetLabAccess": {
                      "type": "boolean",
                      "description": "Whether your account can place orders."
                    },
                    "requestAccessUrl": {
                      "type": "string",
                      "description": "Where to ask for Assay Portal access."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "That provider isn't one we offer"
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "500": {
            "description": "Something went wrong on our side (`internal_error`). Try again."
          }
        },
        "tags": [
          "Assays"
        ]
      }
    },
    "/api/assay-targets": {
      "get": {
        "summary": "Search assay targets",
        "description": "Binding assays measure how well your sequences bind to a target. Use this to find a\ntarget's id.\n\nPut the id in your order's settings: `targetId` for Adaptyv, `targetIds` for A-Alpha\nBio, or `targetAntigen` for Ginkgo.\n\nYour account needs Assay Portal access to search targets.",
        "operationId": "listAssayTargets",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "provider",
            "in": "query",
            "required": true,
            "description": "The lab whose targets to search. An assay type's `targetsProvider` tells you which.",
            "schema": {
              "type": "string",
              "enum": [
                "adaptyv",
                "aalpha",
                "ginkgo"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Text to look for in a target's id, name, aliases or UniProt id. Upper and lower case are treated the same.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many targets to return, up to 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The targets that match",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "provider": {
                      "type": "string"
                    },
                    "targets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "uniprotId": {
                            "type": "string"
                          },
                          "organism": {
                            "type": "string"
                          },
                          "aliases": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "How many targets matched in all."
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "True when more targets matched than were returned. Narrow your search or raise `limit`."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`provider` is missing or not one we offer, or `limit` is out of range"
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "403": {
            "description": "Your account doesn't have Assay Portal access yet (`wetlab_access_required`)"
          },
          "500": {
            "description": "Something went wrong on our side (`internal_error`). Try again."
          },
          "502": {
            "description": "We couldn't load this lab's target list just now (`targets_unavailable`). Try again shortly."
          }
        },
        "tags": [
          "Assays"
        ]
      }
    },
    "/api/submit-assay": {
      "post": {
        "summary": "Submit an assay order",
        "description": "Places an order with a lab.\n\nTamarind reviews every order before a lab starts work on it. A new order starts as\n`pending_approval`. We arrange payment with you, then approve the order. Nothing is\ncharged through the API.\n\nEach order is for one assay type. To check an order without placing it, send the same\nbody to `POST /validate-assay`. For a price and turnaround estimate, send it to\n`POST /estimate-assay`.\n\nSend an `Idempotency-Key` header so that a retry can never place the same order twice.\nIf you repeat a request with the same key and body, you get the original order back.\nA key is remembered for at least 7 days: retry within that window, and use a new key\nfor a new order after it.\n\nWhen something goes wrong, the response includes a `code` your program can check, as\nwell as a message for people. The status codes below list the codes.",
        "operationId": "submitAssay",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "A value you choose that is unique to this order. Send the same one if you retry. Remembered for at least 7 days.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AssayOrderRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The order was placed. If you repeated a request with the same Idempotency-Key, this is the original order and `idempotentReplay` is true.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "assay": {
                      "$ref": "#/components/schemas/AssayOrder"
                    },
                    "idempotentReplay": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Something in the request is wrong. `error` says what, and `details` lists each problem when there are several."
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "403": {
            "description": "Your account doesn't have Assay Portal access yet (`wetlab_access_required`), or you are using a trial key that hasn't been claimed."
          },
          "409": {
            "description": "Nothing was created. The `code` says why.\n\n`idempotency_in_progress`: your earlier request with this key is still running. Wait\na minute and retry with the same key. A request that died can take about 6 minutes\nto clear.\n\n`idempotency_order_unavailable`: the order this key created no longer exists. Use a\nnew key to order again.\n\n`duplicate_order`: you recently placed an order for the same assays and number of\nsequences (see `duplicateOfOrderId`). If you want a second one, resend with\n`confirmDuplicate: true`.\n\n`assays_blocked`: an earlier attempt may already have reached the lab. We have been\nnotified and will follow up, so don't resubmit.\n\n`assays_in_flight`: another submit for these assays is still running, or didn't\nfinish cleanly. Check GET /assays before trying again."
          },
          "422": {
            "description": "You already used this Idempotency-Key for a different order. Use a new key."
          },
          "500": {
            "description": "Something went wrong on our side. If the response has an `assayId`, the order was created, so don't resubmit. If it doesn't, check GET /assays; retrying with the same Idempotency-Key is safe. If you didn't send a key, check GET /assays before you retry, because a retry could place a second order."
          },
          "502": {
            "description": "A problem with the lab. The `code` says what happened.\n\n`provider_error`: the lab failed after we recorded your order. We have been notified,\nand the lab may be holding a draft.\n\n`order_incomplete`: the order exists (see `assayId`) but didn't finish. We will\nfollow up, so don't resubmit.\n\n`quote_unavailable`: we couldn't get a quote from A-Alpha, and nothing was created.\nYou can retry with the same key.\n\nFor `provider_error` and `order_incomplete`, retrying with the same key returns the\nsame answer."
          },
          "503": {
            "description": "A temporary problem, and nothing was created. Retry with the same key. The one exception is when `teamNotified` is true: then a retry won't help, and we will follow up."
          }
        },
        "tags": [
          "Assays"
        ]
      }
    },
    "/api/validate-assay": {
      "post": {
        "summary": "Check an assay order",
        "description": "Checks an order without placing it. Send the body you would send to\n`POST /submit-assay`. Nothing is created, and your sequences are not sent to a lab.\n\nThe answer is a 200 whether or not the order is valid, so read `valid`. When it is\nfalse, `error` says what is wrong, and `details` lists each problem when there are\nseveral.\n\nA valid order isn't compared with your earlier orders, so the real submit can still\ncome back with `duplicate_order`, `assays_blocked` or `assays_in_flight`.",
        "operationId": "validateAssay",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AssayOrderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The verdict. An order with problems is also a 200.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "valid"
                  ],
                  "properties": {
                    "valid": {
                      "type": "boolean"
                    },
                    "assayType": {
                      "type": "string",
                      "description": "Present when valid."
                    },
                    "provider": {
                      "type": "string",
                      "description": "Present when valid."
                    },
                    "modality": {
                      "type": "string",
                      "description": "Present when valid."
                    },
                    "sequenceCount": {
                      "type": "integer",
                      "description": "Present when valid."
                    },
                    "code": {
                      "type": "string",
                      "description": "Present when not valid: `invalid_request`."
                    },
                    "error": {
                      "type": "string",
                      "description": "Present when not valid. What is wrong."
                    },
                    "details": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Each problem",
                      "when there are several.": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "403": {
            "description": "Your account doesn't have Assay Portal access yet (`wetlab_access_required`), or you are using a trial key that hasn't been claimed."
          },
          "500": {
            "description": "Something went wrong on our side (`internal_error`). Try again."
          }
        },
        "tags": [
          "Assays"
        ]
      }
    },
    "/api/estimate-assay": {
      "post": {
        "summary": "Estimate an assay order",
        "description": "Gives the estimated price and turnaround time for an order. Send the body you would\nsend to `POST /submit-assay`. No order is created and nothing is charged.\n\nThe price comes from the lab, so your sequences are sent to the lab to price them. It\nis an estimate, not a quote. Tamarind confirms the\nfinal price with you before your order is approved.\n\nSome targets are priced on request. For those `estimatedCostUsd` is null and `note`\nsays so.",
        "operationId": "estimateAssay",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AssayOrderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The estimate",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "assayType": {
                      "type": "string"
                    },
                    "provider": {
                      "type": "string"
                    },
                    "sequenceCount": {
                      "type": "integer"
                    },
                    "estimatedCostUsd": {
                      "type": "number",
                      "nullable": true,
                      "description": "The estimated price of the whole order",
                      "in US dollars.": null
                    },
                    "costPerSequenceUsd": {
                      "type": "number",
                      "nullable": true,
                      "description": "The estimated price divided by the number of sequences."
                    },
                    "turnaround": {
                      "type": "string",
                      "nullable": true,
                      "description": "The usual turnaround for this assay type.",
                      "example": "2-3 weeks"
                    },
                    "turnaroundDays": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The lab's own turnaround estimate for this order in days",
                      "when it gives one.": null
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The order has a problem (`invalid_request`), or the lab would reject it or can't price it as it is (`quote_rejected`)."
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "403": {
            "description": "Your account doesn't have Assay Portal access yet (`wetlab_access_required`), or you are using a trial key that hasn't been claimed."
          },
          "500": {
            "description": "Something went wrong on our side (`internal_error`). Try again."
          },
          "503": {
            "description": "We couldn't get an estimate from the lab right now (`estimate_unavailable`). Try again shortly. You can place the order without one."
          }
        },
        "tags": [
          "Assays"
        ]
      }
    },
    "/api/assays": {
      "get": {
        "summary": "List or get your assay orders",
        "description": "Shows your assay orders, newest first. To look up one order, pass its `assayId`.\n\nOrders come back a page at a time. When the response has a `nextToken`, send it back\nto get the next page, and keep going until `nextToken` is null. A page can hold fewer\norders than `limit` and still have more to come.\n\nAn order normally goes from `pending_approval` to `approved`, then `in_progress`,\nthen `completed`.",
        "operationId": "listAssays",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "assayId",
            "in": "query",
            "description": "Return just this order. The other parameters are ignored when you send it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many orders to return per page, up to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "nextToken",
            "in": "query",
            "description": "The `nextToken` from the previous page, to get the next one.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Only return orders with this status.",
            "schema": {
              "type": "string",
              "enum": [
                "pending_quote",
                "pending_approval",
                "approved",
                "in_progress",
                "completed",
                "failed",
                "cancelling",
                "cancelled",
                "rejected",
                "unknown"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One order when you pass `assayId`. Otherwise a page of orders.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "title": "Order page",
                      "type": "object",
                      "properties": {
                        "assays": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/AssayOrder"
                          }
                        },
                        "nextToken": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    },
                    {
                      "title": "Single order",
                      "type": "object",
                      "properties": {
                        "assay": {
                          "$ref": "#/components/schemas/AssayOrder"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`limit`, `status` or `nextToken` isn't valid"
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "404": {
            "description": "You don't have an order with this `assayId`"
          },
          "500": {
            "description": "Something went wrong on our side (`internal_error`). Try again."
          }
        },
        "tags": [
          "Assays"
        ]
      }
    },
    "/api/cancel-assay": {
      "post": {
        "summary": "Cancel an assay order",
        "description": "Cancels an order.\n\nIf Tamarind hasn't approved the order yet, it is cancelled straight away. If it has\nalready gone to Ginkgo, it shows as `cancelling` until Ginkgo confirms. Adaptyv orders\ncan't be cancelled through the API, so contact Tamarind support for those.\n\nEach order has a `cancellable` field that tells you whether it can be cancelled right\nnow. Cancelling an order that is already cancelled is fine: you just get it back.",
        "operationId": "cancelAssay",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "assayId"
                ],
                "properties": {
                  "assayId": {
                    "type": "string",
                    "description": "The order to cancel."
                  },
                  "reason": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Why you are cancelling. Optional."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The order, now `cancelled` or `cancelling`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "assay": {
                      "$ref": "#/components/schemas/AssayOrder"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`assayId` is missing, or the order can't be cancelled in its current state (`not_cancellable`). `status` shows where the order is."
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "404": {
            "description": "You don't have an order with this `assayId`"
          },
          "409": {
            "description": "The order can't be cancelled right now. The `code` says why.\n\n`approval_in_progress`: Tamarind is approving it at this moment. Try again in a minute.\n\n`approval_under_review`: an approval didn't finish cleanly and the team is checking\nit. Contact support instead of retrying.\n\n`order_changed`: the order changed while you were cancelling it. Look it up again."
          },
          "500": {
            "description": "Something went wrong on our side (`internal_error`). Try again."
          },
          "502": {
            "description": "We couldn't confirm the cancellation (`cancel_unconfirmed`). Check the order's status before trying again."
          }
        },
        "tags": [
          "Assays"
        ]
      }
    },
    "/api/assay-results": {
      "get": {
        "summary": "Get assay results",
        "description": "Gives you download links for an order's result files.\n\nLabs deliver results some time after an order completes. Until they do, `available`\nis false and `files` is empty, so check again later. Once results arrive they stay\navailable.\n\nThe links stop working after a while (`expiresInSeconds` is the longest they last), so\nask for fresh ones when you need them instead of saving them.",
        "operationId": "getAssayResults",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "assayId",
            "in": "query",
            "required": true,
            "description": "The order to get results for.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The order's result files",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "assayId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "available": {
                      "type": "boolean"
                    },
                    "complete": {
                      "type": "boolean",
                      "description": "True when `files` has every file the lab delivered."
                    },
                    "files": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "missingFiles": {
                      "type": "array",
                      "description": "Names of files the lab delivered that you can't download yet.",
                      "items": {
                        "type": "string"
                      }
                    },
                    "expiresInSeconds": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`assayId` is missing"
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "404": {
            "description": "You don't have an order with this `assayId`"
          },
          "500": {
            "description": "Something went wrong on our side (`internal_error`). Try again."
          },
          "502": {
            "description": "We couldn't load this order's results just now (`results_unavailable`). Try again shortly, or contact support if it keeps happening."
          }
        },
        "tags": [
          "Assays"
        ]
      }
    },
    "/api/molecules/groups": {
      "get": {
        "tags": [
          "molecules-public"
        ],
        "summary": "List Groups",
        "description": "List your molecule groups.\n\nA group is a named collection of molecules. Pass `scope=org` to include your whole organization.\n\nPaginated — follow `nextCursor` until it is null.",
        "operationId": "listGroups",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 200
                },
                {
                  "type": "null"
                }
              ],
              "description": "Case-insensitive substring match on the group's name.",
              "title": "Search"
            },
            "description": "Case-insensitive substring match on the group's name."
          },
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "maxItems": 32
                },
                {
                  "type": "null"
                }
              ],
              "description": "One condition on the group's own metadata, written `field:operator:value`. Repeat it and a group must pass every condition. Operators: `gt`, `gte`, `lt`, `lte`, `eq`, `neq`, `iequals`, `includes`, `in`, and `exists`, written `field:exists` with no value. `in` takes a set of values separated by `|` and matches a value equal to any of them (`stage:in:lead|backup`): each member matches the stored text ignoring case, and a member that is a number also matches that number stored as a number or as text (`2` finds a stored `2.0`). A member's text is compared as written, spaces included. Blank members are dropped, a set with no member left is refused, and a set holds at most 64 members. A dotted field names a metadata key here, never a tool score, for every operator (`stage.phase` reads the key `stage.phase`, so `stage.phase:in:1|2` does too): groups carry no scores, so nothing on this endpoint reads one. At most 32 conditions.",
              "title": "Filter"
            },
            "description": "One condition on the group's own metadata, written `field:operator:value`. Repeat it and a group must pass every condition. Operators: `gt`, `gte`, `lt`, `lte`, `eq`, `neq`, `iequals`, `includes`, `in`, and `exists`, written `field:exists` with no value. `in` takes a set of values separated by `|` and matches a value equal to any of them (`stage:in:lead|backup`): each member matches the stored text ignoring case, and a member that is a number also matches that number stored as a number or as text (`2` finds a stored `2.0`). A member's text is compared as written, spaces included. Blank members are dropped, a set with no member left is refused, and a set holds at most 64 members. A dotted field names a metadata key here, never a tool score, for every operator (`stage.phase` reads the key `stage.phase`, so `stage.phase:in:1|2` does too): groups carry no scores, so nothing on this endpoint reads one. At most 32 conditions."
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^(mine|org)$",
              "description": "`mine` (the default) shows groups you created; `org` shows every group in your organization.",
              "default": "mine",
              "title": "Scope"
            },
            "description": "`mine` (the default) shows groups you created; `org` shows every group in your organization."
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^(recent|name|size)$",
              "description": "Sort by `recent` (the default, by creation date), `name`, or `size` (how many molecules the group holds). Pair with `dir`.",
              "default": "recent",
              "title": "Sort"
            },
            "description": "Sort by `recent` (the default, by creation date), `name`, or `size` (how many molecules the group holds). Pair with `dir`."
          },
          {
            "name": "dir",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^(asc|desc)$",
              "description": "Sort direction, `asc` or `desc`.",
              "default": "desc",
              "title": "Dir"
            },
            "description": "Sort direction, `asc` or `desc`."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 200,
              "minimum": 1,
              "description": "Maximum number of items to return in one page.",
              "default": 50,
              "title": "Limit"
            },
            "description": "Maximum number of items to return in one page."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 4096
                },
                {
                  "type": "null"
                }
              ],
              "description": "Pagination token from the previous response's `nextCursor`. Omit for the first page; follow `nextCursor` until null (an empty `items` page while `nextCursor` is set is normal).",
              "title": "Cursor"
            },
            "description": "Pagination token from the previous response's `nextCursor`. Omit for the first page; follow `nextCursor` until null (an empty `items` page while `nextCursor` is set is normal)."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicGroupPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Create Group",
        "description": "Create an empty molecule group.\n\nAdd molecules with `POST /molecules/upload` (JSON) or `POST /molecules/import-file` (a file).\n\nBind a `schemaId` to require every molecule to match that schema.",
        "operationId": "createGroup",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicCreateGroupRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicGroup"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/molecules/groups/{group_id}": {
      "get": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Get Group",
        "description": "Fetch one group by id — its name, size, and origin.",
        "operationId": "getGroup",
        "parameters": [
          {
            "name": "group_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Group Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicGroup"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/molecules/groups/{group_id}/score-observations": {
      "post": {
        "tags": [
          "molecules-public"
        ],
        "summary": "List Group Score Observations",
        "description": "List retained seed/sample observations for explicit concrete jobs in this group.\n\nEvery requested job must be visible, concrete, eligible, terminally complete,\nand output-ingested. Every item carries its actual producing `jobId` and `jobName`.\n\nThis is an advanced, read-only API. It returns observations in bulk and does not change the\nordinary molecule sheet response or any frontend UI.",
        "operationId": "listGroupScoreObservations",
        "parameters": [
          {
            "name": "group_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Group Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicScoreObservationQuery"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicScoreObservationPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/molecules/schemas": {
      "get": {
        "tags": [
          "molecules-public"
        ],
        "summary": "List Schemas",
        "description": "List your schemas.\n\nPass `scope=org` to include every schema in your organization.",
        "operationId": "listSchemas",
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^(mine|org)$",
              "default": "mine",
              "title": "Scope"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 200,
              "minimum": 1,
              "description": "Maximum number of items to return in one page.",
              "default": 50,
              "title": "Limit"
            },
            "description": "Maximum number of items to return in one page."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 4096
                },
                {
                  "type": "null"
                }
              ],
              "description": "Pagination token from the previous response's `nextCursor`. Omit for the first page; follow `nextCursor` until null (an empty `items` page while `nextCursor` is set is normal).",
              "title": "Cursor"
            },
            "description": "Pagination token from the previous response's `nextCursor`. Omit for the first page; follow `nextCursor` until null (an empty `items` page while `nextCursor` is set is normal)."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicSchemaPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Create Schema",
        "description": "Define a reusable set of typed fields you can bind to a group.\n\nScalar fields (`string`, `integer`, `float`, `boolean`, `category`) constrain a molecule's metadata.\n\nA `chain` field describes a required chain, named by its `name`.",
        "operationId": "createSchema",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicCreateSchemaRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicSchema"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/molecules/schemas/{schema_id}": {
      "get": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Get Schema",
        "description": "Fetch one schema by id.",
        "operationId": "getSchema",
        "parameters": [
          {
            "name": "schema_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Schema Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicSchema"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Update Schema",
        "description": "Update a schema's name and/or fields.\n\nSending `fields` replaces the whole list.\n\nMolecules already published to bound groups are left as they are. Imports that have not\npublished yet are validated against the current schema at publication.",
        "operationId": "updateSchema",
        "parameters": [
          {
            "name": "schema_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Schema Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicUpdateSchemaRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicSchema"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/molecules/remove": {
      "post": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Remove Molecules",
        "description": "Remove molecules from a group.\n\nThis detaches group membership; it does not delete the molecule, which stays in any other groups with its scores and files intact. To delete a molecule everywhere, use `DELETE /molecules/{moleculeId}`.\n\nIdempotent — ids not in the group are ignored, and `removedIds` lists what was detached.",
        "operationId": "removeMolecules",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicRemoveMoleculesFromGroupRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicRemoveMoleculesResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/molecules/upload": {
      "post": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Upload Molecules",
        "description": "Queue molecules for JSON upload ingestion.\n\nReturns HTTP 202 with the exact content-addressed molecule ids and an import id.\nPoll `GET /molecules/imports/{importId}` until the import reaches `ingested`,\n`failed`, or `cancelled`; the ids become readable only after ingestion succeeds.",
        "operationId": "uploadMolecules",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicUploadRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicUploadResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/molecules/import-file": {
      "post": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Start a file import",
        "description": "Import molecules from a file. Step 1 of 3.\n\nThen `PUT` the bytes to `uploadUrl`, and `POST\n/molecules/imports/{importId}/commit` to enqueue ingestion.\n\nFor a CSV, describe your chain columns with `chainMapping` (keyed by chain id);\n`columnMapping` handles the scalar columns only. For `pdb`/`sdf`/`fasta`/`zip`\nthe chains are read from the file.",
        "operationId": "importFile",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicFileImportRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicFileImportStart"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/molecules/imports/{import_id}": {
      "get": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Check import status",
        "description": "Poll an import's progress after upload or commit.\n\nStatus moves `created` → `uploaded` → `queued` → `ingested` (or `failed` / `cancelled`).\n\nIt reflects this import specifically, not the target group's overall state.",
        "operationId": "getImport",
        "parameters": [
          {
            "name": "import_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Import Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicImportStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Cancel or delete an import",
        "description": "Cancel an unclaimed import and clean its source objects.",
        "operationId": "deleteImport",
        "parameters": [
          {
            "name": "import_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Import Id"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/molecules/imports/{import_id}/commit": {
      "post": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Commit an import",
        "description": "Step 3 of 3: enqueue ingestion for the uploaded file.\n\n- Returns `status: \"queued\"` immediately by default — poll `GET /molecules/imports/{importId}`.",
        "operationId": "commitImport",
        "parameters": [
          {
            "name": "import_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Import Id"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/PublicCommitRequest"
                  },
                  {
                    "type": "null"
                  }
                ],
                "title": "Body"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicCommitQueued"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/molecules": {
      "get": {
        "tags": [
          "molecules-public"
        ],
        "summary": "List Molecules",
        "description": "List molecules in your organization. Set `scope=org` to search your organization.\n\nIn the full view, each molecule includes its chains, scores, metadata, and files inline. Search\nby name or sequence, or filter by group, `owner`, `jobId`, `jobName`, property, or score.\n\nResults are returned in pages. Use `nextCursor` to retrieve more results.",
        "operationId": "listMolecules",
        "parameters": [
          {
            "name": "group",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 200
                },
                {
                  "type": "null"
                }
              ],
              "description": "Limit results to one group by ID (retrieve ID using `/api/molecules/groups`).",
              "title": "Group"
            },
            "description": "Limit results to one group by ID (retrieve ID using `/api/molecules/groups`)."
          },
          {
            "name": "jobId",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 200
                },
                {
                  "type": "null"
                }
              ],
              "description": "Limit results to one job's input and output molecules by ID. For batches, use the batch's ID to retrieve molecules for all jobs within the batch.",
              "title": "Jobid"
            },
            "description": "Limit results to one job's input and output molecules by ID. For batches, use the batch's ID to retrieve molecules for all jobs within the batch."
          },
          {
            "name": "jobName",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 200
                },
                {
                  "type": "null"
                }
              ],
              "description": "List input and output molecules by job or batch name.",
              "title": "Jobname"
            },
            "description": "List input and output molecules by job or batch name."
          },
          {
            "name": "owner",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 320
                },
                {
                  "type": "null"
                }
              ],
              "description": "Only groups whose owner equals this value, as the owners list returns it.",
              "title": "Owner"
            },
            "description": "Only groups whose owner equals this value, as the owners list returns it."
          },
          {
            "name": "nodeRunId",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 200
                },
                {
                  "type": "null"
                }
              ],
              "description": "Limit results to one pipeline step's output molecules by node run ID (retrieve ID using `/pipelines/runs/{run_id}`).",
              "title": "Noderunid"
            },
            "description": "Limit results to one pipeline step's output molecules by node run ID (retrieve ID using `/pipelines/runs/{run_id}`)."
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/MoleculeType"
                  },
                  "maxItems": 32
                },
                {
                  "type": "null"
                }
              ],
              "description": "Filter by molecule type, such as `protein`, `small_molecule`, or `nucleic_acid`.",
              "title": "Type"
            },
            "description": "Filter by molecule type, such as `protein`, `small_molecule`, or `nucleic_acid`."
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 4000
                },
                {
                  "type": "null"
                }
              ],
              "description": "Enter a search term to find molecule names, user-uploaded property names, or tool score names. To search chains instead, set `mode=sequence` and enter a sequence or exact SMILES. Use at least 3 characters for sequence searches or when searching without a group.",
              "title": "Search"
            },
            "description": "Enter a search term to find molecule names, user-uploaded property names, or tool score names. To search chains instead, set `mode=sequence` and enter a sequence or exact SMILES. Use at least 3 characters for sequence searches or when searching without a group."
          },
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^(default|name|sequence)$",
              "description": "Choose what `search` matches: `sequence` searches amino acid chains by exact or subsequence match, while `sequenceMatch=exact_smiles` matches a small molecule's stored SMILES exactly. `default`/`name` match molecule, property, and score names.",
              "default": "default",
              "title": "Mode"
            },
            "description": "Choose what `search` matches: `sequence` searches amino acid chains by exact or subsequence match, while `sequenceMatch=exact_smiles` matches a small molecule's stored SMILES exactly. `default`/`name` match molecule, property, and score names."
          },
          {
            "name": "sequenceMatch",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^(subsequence|exact|exact_smiles)$",
              "description": "Only available with `mode=sequence`: `subsequence` matches part of an amino-acid chain; `exact` matches a whole amino-acid chain; `exact_smiles` matches a whole small-molecule chain exactly, including case.",
              "default": "subsequence",
              "title": "Sequencematch"
            },
            "description": "Only available with `mode=sequence`: `subsequence` matches part of an amino-acid chain; `exact` matches a whole amino-acid chain; `exact_smiles` matches a whole small-molecule chain exactly, including case."
          },
          {
            "name": "moleculesPerGroup",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "integer",
                  "maximum": 25,
                  "minimum": 1
                },
                {
                  "type": "null"
                }
              ],
              "description": "Maximum molecules per group across the whole result set. Requires `sortBy=groupName`; unavailable with `mode=sequence`.",
              "title": "Moleculespergroup"
            },
            "description": "Maximum molecules per group across the whole result set. Requires `sortBy=groupName`; unavailable with `mode=sequence`."
          },
          {
            "name": "examplesPerGroup",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "integer",
                  "maximum": 25,
                  "minimum": 1
                },
                {
                  "type": "null"
                }
              ],
              "description": "Deprecated: the old name for `moleculesPerGroup`.",
              "deprecated": true,
              "title": "Examplespergroup"
            },
            "description": "Deprecated: the old name for `moleculesPerGroup`.",
            "deprecated": true
          },
          {
            "name": "sequence",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 200
                },
                {
                  "type": "null"
                }
              ],
              "description": "Match molecules containing this amino-acid subsequence in any chain (case-insensitive, at least 3 characters).",
              "title": "Sequence"
            },
            "description": "Match molecules containing this amino-acid subsequence in any chain (case-insensitive, at least 3 characters)."
          },
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "maxItems": 32
                },
                {
                  "type": "null"
                }
              ],
              "description": "String in format `field:operator:value`. \"field\" can be user-uploaded properties or tool scores (in format `tool-id.score`). Supported operators: `gt`, `gte`, `lt`, `lte`, `eq`, `neq`, `iequals`, `includes`, `in` (any of a `|`-separated set of at most 64 members, e.g. `stage:in:lead|backup`), `exists` (written `field:exists`, no value). Append `:jobId` to a tool score (not with `in` or `exists`) to filter only from tool scores from a particular job. Repeat `filter` to combine conditions; a molecule must match all of them. See `/tools` endpoint for list of output scores for each tool.",
              "title": "Filter"
            },
            "description": "String in format `field:operator:value`. \"field\" can be user-uploaded properties or tool scores (in format `tool-id.score`). Supported operators: `gt`, `gte`, `lt`, `lte`, `eq`, `neq`, `iequals`, `includes`, `in` (any of a `|`-separated set of at most 64 members, e.g. `stage:in:lead|backup`), `exists` (written `field:exists`, no value). Append `:jobId` to a tool score (not with `in` or `exists`) to filter only from tool scores from a particular job. Repeat `filter` to combine conditions; a molecule must match all of them. See `/tools` endpoint for list of output scores for each tool."
          },
          {
            "name": "tags",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "maxItems": 32
                },
                {
                  "type": "null"
                }
              ],
              "description": "Limit results to molecules carrying this tag. Repeat it to require several tags.",
              "title": "Tags"
            },
            "description": "Limit results to molecules carrying this tag. Repeat it to require several tags."
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^(mine|org)$",
              "description": "`mine` (the default) shows molecules in groups you created; `org` shows everything across your organization.",
              "default": "mine",
              "title": "Scope"
            },
            "description": "`mine` (the default) shows molecules in groups you created; `org` shows everything across your organization."
          },
          {
            "name": "sortBy",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200,
              "description": "Sort by `id`, `added`, `type`, `groupName`, a property, or a score such as `alphafold.ptm`. Scores use the tool's \"best run.\" `groupName` keeps each group together; use `moleculesPerGroup` to limit its results.",
              "default": "added",
              "title": "Sortby"
            },
            "description": "Sort by `id`, `added`, `type`, `groupName`, a property, or a score such as `alphafold.ptm`. Scores use the tool's \"best run.\" `groupName` keeps each group together; use `moleculesPerGroup` to limit its results."
          },
          {
            "name": "dir",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^(asc|desc)$",
              "description": "`asc`: ascending order. `desc`: descending order.",
              "default": "desc",
              "title": "Dir"
            },
            "description": "`asc`: ascending order. `desc`: descending order."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 200,
              "minimum": 1,
              "description": "Maximum number of items to return in one page.",
              "default": 50,
              "title": "Limit"
            },
            "description": "Maximum number of items to return in one page."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 4096
                },
                {
                  "type": "null"
                }
              ],
              "description": "Omit for your first request. To get the next page, copy the `nextCursor` value from the response into this parameter and send another request with the same filters. Repeat until `nextCursor` is null, even if a page is empty.",
              "title": "Cursor"
            },
            "description": "Omit for your first request. To get the next page, copy the `nextCursor` value from the response into this parameter and send another request with the same filters. Repeat until `nextCursor` is null, even if a page is empty."
          },
          {
            "name": "view",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "full",
                "card"
              ],
              "type": "string",
              "description": "Return full molecules (default) or molecule cards.",
              "default": "full",
              "title": "View"
            },
            "description": "Return full molecules (default) or molecule cards."
          },
          {
            "name": "searchCaps",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 1024
                },
                {
                  "type": "null"
                }
              ],
              "description": "Comma-separated list of capabilities. The response's `caps` field lists the values the API supports from your request.",
              "title": "Searchcaps"
            },
            "description": "Comma-separated list of capabilities. The response's `caps` field lists the values the API supports from your request."
          }
        ],
        "responses": {
          "200": {
            "description": "The response contains full molecules by default. Set `view=card` to return molecule cards. If you set `searchCaps`, the response includes a `caps` field listing the capabilities supported from your request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicMoleculePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/molecules/search": {
      "post": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Search Molecules",
        "description": "Search molecules using the same filters as `GET /molecules`. Send repeated filters as arrays. Maximum request body: 4 MB.",
        "operationId": "searchMolecules",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "group": {
                    "anyOf": [
                      {
                        "type": "string",
                        "maxLength": 200
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "title": "Group",
                    "description": "Limit results to one group by ID (retrieve ID using `/api/molecules/groups`)."
                  },
                  "jobId": {
                    "anyOf": [
                      {
                        "type": "string",
                        "maxLength": 200
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "title": "Jobid",
                    "description": "Limit results to one job's input and output molecules by ID. For batches, use the batch's ID to retrieve molecules for all jobs within the batch."
                  },
                  "jobName": {
                    "anyOf": [
                      {
                        "type": "string",
                        "maxLength": 200
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "title": "Jobname",
                    "description": "List input and output molecules by job or batch name."
                  },
                  "owner": {
                    "anyOf": [
                      {
                        "type": "string",
                        "maxLength": 320
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "title": "Owner",
                    "description": "Only groups whose owner equals this value, as the owners list returns it."
                  },
                  "nodeRunId": {
                    "anyOf": [
                      {
                        "type": "string",
                        "maxLength": 200
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "title": "Noderunid",
                    "description": "Limit results to one pipeline step's output molecules by node run ID (retrieve ID using `/pipelines/runs/{run_id}`)."
                  },
                  "type": {
                    "anyOf": [
                      {
                        "items": {
                          "type": "string",
                          "enum": [
                            "protein",
                            "antibody",
                            "peptide",
                            "enzyme",
                            "small_molecule",
                            "nucleic_acid",
                            "small_molecule_binding_protein"
                          ],
                          "title": "MoleculeType",
                          "description": "The spec's `MoleculeType` — the kind of a molecule, and of the molecules a\ngroup holds.\n\nValue-identical to the internal `Modality` (the user-picked upload modality),\nwhich is the superset enum `complexes.type` is written from. Declared\nseparately because it is a PUBLISHED contract: `Modality` is free to grow a\nvalue for an internal picker without that value silently becoming part of the\npublic API. `public_types_match_modality` pins them equal today."
                        },
                        "type": "array",
                        "maxItems": 32
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "title": "Type",
                    "description": "Filter by molecule type, such as `protein`, `small_molecule`, or `nucleic_acid`."
                  },
                  "search": {
                    "anyOf": [
                      {
                        "type": "string",
                        "maxLength": 4000
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "title": "Search",
                    "description": "Enter a search term to find molecule names, user-uploaded property names, or tool score names. To search chains instead, set `mode=sequence` and enter a sequence or exact SMILES. Use at least 3 characters for sequence searches or when searching without a group."
                  },
                  "mode": {
                    "type": "string",
                    "pattern": "^(default|name|sequence)$",
                    "title": "Mode",
                    "description": "Choose what `search` matches: `sequence` searches amino acid chains by exact or subsequence match, while `sequenceMatch=exact_smiles` matches a small molecule's stored SMILES exactly. `default`/`name` match molecule, property, and score names.",
                    "default": "default"
                  },
                  "sequenceMatch": {
                    "type": "string",
                    "pattern": "^(subsequence|exact|exact_smiles)$",
                    "title": "Sequencematch",
                    "description": "Only available with `mode=sequence`: `subsequence` matches part of an amino-acid chain; `exact` matches a whole amino-acid chain; `exact_smiles` matches a whole small-molecule chain exactly, including case.",
                    "default": "subsequence"
                  },
                  "moleculesPerGroup": {
                    "anyOf": [
                      {
                        "type": "integer",
                        "maximum": 25,
                        "minimum": 1
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "title": "Moleculespergroup",
                    "description": "Maximum molecules per group across the whole result set. Requires `sortBy=groupName`; unavailable with `mode=sequence`."
                  },
                  "sequence": {
                    "anyOf": [
                      {
                        "type": "string",
                        "maxLength": 200
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "title": "Sequence",
                    "description": "Match molecules containing this amino-acid subsequence in any chain (case-insensitive, at least 3 characters)."
                  },
                  "filter": {
                    "anyOf": [
                      {
                        "items": {
                          "type": "string"
                        },
                        "type": "array",
                        "maxItems": 32
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "title": "Filter",
                    "description": "String in format `field:operator:value`. \"field\" can be user-uploaded properties or tool scores (in format `tool-id.score`). Supported operators: `gt`, `gte`, `lt`, `lte`, `eq`, `neq`, `iequals`, `includes`, `in` (any of a `|`-separated set of at most 64 members, e.g. `stage:in:lead|backup`), `exists` (written `field:exists`, no value). Append `:jobId` to a tool score (not with `in` or `exists`) to filter only from tool scores from a particular job. Repeat `filter` to combine conditions; a molecule must match all of them. See `/tools` endpoint for list of output scores for each tool."
                  },
                  "tags": {
                    "anyOf": [
                      {
                        "items": {
                          "type": "string"
                        },
                        "type": "array",
                        "maxItems": 32
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "title": "Tags",
                    "description": "Limit results to molecules carrying this tag. Repeat it to require several tags."
                  },
                  "scope": {
                    "type": "string",
                    "pattern": "^(mine|org)$",
                    "title": "Scope",
                    "description": "`mine` (the default) shows molecules in groups you created; `org` shows everything across your organization.",
                    "default": "mine"
                  },
                  "sortBy": {
                    "type": "string",
                    "maxLength": 200,
                    "title": "Sortby",
                    "description": "Sort by `id`, `added`, `type`, `groupName`, a property, or a score such as `alphafold.ptm`. Scores use the tool's \"best run.\" `groupName` keeps each group together; use `moleculesPerGroup` to limit its results.",
                    "default": "added"
                  },
                  "dir": {
                    "type": "string",
                    "pattern": "^(asc|desc)$",
                    "title": "Dir",
                    "description": "`asc`: ascending order. `desc`: descending order.",
                    "default": "desc"
                  },
                  "limit": {
                    "type": "integer",
                    "maximum": 200,
                    "minimum": 1,
                    "title": "Limit",
                    "description": "Maximum number of items to return in one page.",
                    "default": 50
                  },
                  "cursor": {
                    "anyOf": [
                      {
                        "type": "string",
                        "maxLength": 4096,
                        "minLength": 1
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "title": "Cursor",
                    "description": "Omit for your first request. To get the next page, copy the `nextCursor` value from the response into this parameter and send another request with the same filters. Repeat until `nextCursor` is null, even if a page is empty."
                  },
                  "view": {
                    "type": "string",
                    "enum": [
                      "full",
                      "card"
                    ],
                    "title": "View",
                    "description": "Return full molecules (default) or molecule cards.",
                    "default": "full"
                  },
                  "searchCaps": {
                    "anyOf": [
                      {
                        "type": "string",
                        "maxLength": 1024
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "title": "Searchcaps",
                    "description": "Comma-separated list of capabilities. The response's `caps` field lists the values the API supports from your request."
                  }
                },
                "additionalProperties": false,
                "type": "object",
                "title": "PublicMoleculeSearchRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/PublicMoleculePage"
                    },
                    {
                      "$ref": "#/components/schemas/PublicMoleculeCardPage"
                    },
                    {
                      "$ref": "#/components/schemas/PublicMoleculeCapsPage"
                    },
                    {
                      "$ref": "#/components/schemas/PublicMoleculeCardCapsPage"
                    }
                  ],
                  "title": "Response Searchmolecules"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "description": "Request Entity Too Large",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicProblem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/molecules/{molecule_id}/metadata": {
      "patch": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Update Molecule Metadata",
        "description": "Update a molecule. All fields are optional and applied together; `null` clears a field.\n\nMerge your own annotations with `properties` (tool scores aren't editable here). Set the derived-from molecule with `source`, this group's primary structure file with `fileId`, and the per-group display name with `name`.\n\nA `name` already used in the group returns `409`.",
        "operationId": "updateMoleculeMetadata",
        "parameters": [
          {
            "name": "molecule_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Molecule Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicUpdateMetadataRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicMolecule"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/molecules/{molecule_id}/groups": {
      "get": {
        "tags": [
          "molecules-public"
        ],
        "summary": "List Molecule Groups",
        "description": "List every group a molecule belongs to.\n\nUse this when `GET /molecules/{moleculeId}` marks `groups` as truncated. Only your own groups are listed.\n\nPaginated — follow `nextCursor` until it is null.",
        "operationId": "listMoleculeGroups",
        "parameters": [
          {
            "name": "molecule_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Molecule Id"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 200,
              "minimum": 1,
              "description": "Maximum number of items to return in one page.",
              "default": 50,
              "title": "Limit"
            },
            "description": "Maximum number of items to return in one page."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 4096
                },
                {
                  "type": "null"
                }
              ],
              "description": "Pagination token from the previous response's `nextCursor`. Omit for the first page; follow `nextCursor` until null (an empty `items` page while `nextCursor` is set is normal).",
              "title": "Cursor"
            },
            "description": "Pagination token from the previous response's `nextCursor`. Omit for the first page; follow `nextCursor` until null (an empty `items` page while `nextCursor` is set is normal)."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicGroupPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/molecules/{molecule_id}": {
      "get": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Get Molecule",
        "description": "Get one molecule — its chains, scores, files, provenance, and groups, all inline.\n\nAddressed by its id; no group needed.\n\nChain labels are per-group, so pass `groupId` to choose which group's labels you get, else the most recent group wins.",
        "operationId": "getMolecule",
        "parameters": [
          {
            "name": "molecule_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Molecule Id"
            }
          },
          {
            "name": "groupId",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 200
                },
                {
                  "type": "null"
                }
              ],
              "title": "Groupid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicMolecule"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "molecules-public"
        ],
        "summary": "Delete Molecule",
        "description": "Permanently delete a molecule. Admin only.\n\nRemoves the molecule and all its group memberships, scores, and file links across the organization. To remove it from one group instead, use `POST /molecules/remove`.\n\nIdempotent — an unknown id returns `deleted: false`.",
        "operationId": "deleteMolecule",
        "parameters": [
          {
            "name": "molecule_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Molecule Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicDeleteMoleculeResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/pipelines/templates": {
      "post": {
        "tags": [
          "pipelines-public"
        ],
        "summary": "Create a pipeline template",
        "description": "Create a pipeline template from a `pipeline` graph of inputs, tools, and filters.\n\n- The graph is validated on create\n- This first save becomes the template's first version; every later save adds a new immutable version",
        "operationId": "createTemplate",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicCreateTemplateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicTemplate"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-doc-group": "Pipelines"
      },
      "get": {
        "tags": [
          "pipelines-public"
        ],
        "summary": "List pipeline templates",
        "description": "List pipeline templates in your account or organization.",
        "operationId": "listTemplates",
        "parameters": [
          {
            "name": "isPublished",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "boolean"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Filter to published (`true`) or unpublished (`false`) templates.",
              "title": "Ispublished"
            },
            "description": "Filter to published (`true`) or unpublished (`false`) templates."
          },
          {
            "name": "owner",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "enum": [
                    "mine",
                    "org"
                  ],
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Whose templates to list: `mine` (the default) or `org` for your whole organization.",
              "title": "Owner"
            },
            "description": "Whose templates to list: `mine` (the default) or `org` for your whole organization."
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 200
                },
                {
                  "type": "null"
                }
              ],
              "description": "Search pipelines by name.",
              "title": "Search"
            },
            "description": "Search pipelines by name."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 200,
              "minimum": 1,
              "description": "Maximum number of templates to return in one page.",
              "default": 50,
              "title": "Limit"
            },
            "description": "Maximum number of templates to return in one page."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 4096
                },
                {
                  "type": "null"
                }
              ],
              "description": "Pagination token from the previous response's `nextCursor`.",
              "title": "Cursor"
            },
            "description": "Pagination token from the previous response's `nextCursor`."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicTemplatePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-doc-group": "Pipelines"
      }
    },
    "/api/pipelines/templates/{template_id}": {
      "get": {
        "tags": [
          "pipelines-public"
        ],
        "summary": "Get a pipeline template",
        "description": "Retrieve a template by id to view its nodes and required inputs.",
        "operationId": "getTemplate",
        "parameters": [
          {
            "name": "template_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Template Id"
            }
          },
          {
            "name": "version",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "minLength": 1
                },
                {
                  "type": "null"
                }
              ],
              "description": "A specific version to view (a `vN` handle); omit for the current version.",
              "title": "Version"
            },
            "description": "A specific version to view (a `vN` handle); omit for the current version."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicTemplate"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-doc-group": "Pipelines"
      },
      "delete": {
        "tags": [
          "pipelines-public"
        ],
        "summary": "Archive a pipeline template",
        "description": "Archive a template. Its versions and existing runs remain available historically.",
        "operationId": "deleteTemplate",
        "parameters": [
          {
            "name": "template_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Template Id"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-doc-group": "Pipelines"
      }
    },
    "/api/pipelines/templates/{template_id}/publish": {
      "post": {
        "tags": [
          "pipelines-public"
        ],
        "summary": "Publish a template version",
        "description": "Publish a version of your pipeline template to your organization.\n\nOthers in your organization may only run pipelines published to the organization\n\nOnly one version can be published at a time",
        "operationId": "publishTemplate",
        "parameters": [
          {
            "name": "template_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Template Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicPublishRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicPublishResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-doc-group": "Pipelines"
      }
    },
    "/api/pipelines/templates/{template_id}/duplicate": {
      "post": {
        "tags": [
          "pipelines-public"
        ],
        "summary": "Duplicate a pipeline template",
        "description": "Create a copy of a version of your existing pipeline (latest by default).",
        "operationId": "duplicateTemplate",
        "parameters": [
          {
            "name": "template_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Template Id"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/PublicDuplicateTemplateRequest"
                  },
                  {
                    "type": "null"
                  }
                ],
                "title": "Body"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicTemplate"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-doc-group": "Pipelines"
      }
    },
    "/api/pipelines/templates/{template_id}/validate": {
      "post": {
        "tags": [
          "pipelines-public"
        ],
        "summary": "Validate a template",
        "description": "Validate the settings and inputs of a template without creating or executing it.\n\nValidates your pipeline to ensure compatible connections, required tool settings, and valid graph structure.",
        "operationId": "validateTemplate",
        "parameters": [
          {
            "name": "template_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Template Id"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/PublicValidateTemplateRequest"
                  },
                  {
                    "type": "null"
                  }
                ],
                "title": "Body"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicValidateResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-doc-group": "Pipelines"
      }
    },
    "/api/pipelines/submit": {
      "post": {
        "tags": [
          "pipelines-public"
        ],
        "summary": "Submit a pipeline run",
        "description": "Submit a pipeline run from an existing template, or from a new pipeline graph.\n\nProvide a `bindings` map, which defines the molecules/files you want to use for each input node in your pipeline. If your input contains chains, define the mapping of chain IDs between your molecule and the pipeline's reference/default (if running against an existing template).\n\nBindings may be defined as an array of sequences, smiles, or sdf/pdb files (uploaded using the /upload endpoint), or an existing molecule group id (created using /molecules/groups below).\n\nIf applicable, put residue selections inside each binding's `residuesByChain` for the tools that require hotspots / designed residues. Two shapes, auto-detected by the value: SIMPLE `{chain: ranges}` (a chain maps to a range string) fans one selection to every residue field the input feeds; ADVANCED `{toolNodeId: {field: {chain: ranges}}}` (a node maps to a per-field object) picks per (tool node, field). An inline submit is stamped Simplified/Advanced from the shape you send; picks under each binding are merged. Chain and residue selections can also be supplied in tool-node `settings` or top-level per-node `settings`. Reference chain labels follow `chainMapping`. Conflicting inline and binding residue selections are rejected.\n\nThe response returns as soon as the run is accepted, with status `queued` and no node runs yet. Poll `GET /pipelines/runs/{run_id}` for progress; it returns 404 for the few seconds until the engine records the run, and problems found while the engine resolves the run are reported there as a failed run.\n\nChoose from your pipelines or example templates to view example scripts.",
        "operationId": "submitPipeline",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 255
                },
                {
                  "type": "null"
                }
              ],
              "title": "Idempotency-Key"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicSubmitPipelineRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicRun"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-doc-group": "Pipelines"
      }
    },
    "/api/pipelines/validate": {
      "post": {
        "tags": [
          "pipelines-public"
        ],
        "summary": "Validate a pipeline run",
        "description": "Validate the settings and inputs of a run without creating or executing it. Uses the same settings as `/submit`.\n\nValidates your pipeline to ensure compatible connections, required tool settings, and valid graph structure.",
        "operationId": "validateRun",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicSubmitPipelineRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicValidateResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-doc-group": "Pipelines"
      }
    },
    "/api/pipelines/runs": {
      "get": {
        "tags": [
          "pipelines-public"
        ],
        "summary": "List pipeline runs",
        "description": "List pipeline runs in your organization.",
        "operationId": "listRuns",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/RunStatus"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Filter by run status.",
              "title": "Status"
            },
            "description": "Filter by run status."
          },
          {
            "name": "templateId",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "minLength": 1
                },
                {
                  "type": "null"
                }
              ],
              "description": "Filter to runs of one pipeline (by template id).",
              "title": "Templateid"
            },
            "description": "Filter to runs of one pipeline (by template id)."
          },
          {
            "name": "owner",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "enum": [
                    "mine",
                    "org"
                  ],
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Whose runs to list: `mine` (the default) or `org` for your whole organization.",
              "title": "Owner"
            },
            "description": "Whose runs to list: `mine` (the default) or `org` for your whole organization."
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "enum": [
                    "test",
                    "production"
                  ],
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Filter by run source: `test` or `production`.",
              "title": "Source"
            },
            "description": "Filter by run source: `test` or `production`."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 200,
              "minimum": 1,
              "description": "Maximum number of runs to return in one page.",
              "default": 50,
              "title": "Limit"
            },
            "description": "Maximum number of runs to return in one page."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 4096
                },
                {
                  "type": "null"
                }
              ],
              "description": "Pagination token from the previous response's `nextCursor`.",
              "title": "Cursor"
            },
            "description": "Pagination token from the previous response's `nextCursor`."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicRunPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-doc-group": "Pipelines"
      }
    },
    "/api/pipelines/runs/results": {
      "get": {
        "tags": [
          "pipelines-public"
        ],
        "summary": "Download a pipeline run's results",
        "description": "Download full raw results for pipeline by its job name",
        "operationId": "getRunResults",
        "parameters": [
          {
            "name": "jobName",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "description": "The run's job name.",
              "title": "Jobname"
            },
            "description": "The run's job name."
          },
          {
            "name": "user",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 320
                },
                {
                  "type": "null"
                }
              ],
              "description": "The run owner's email — needed only to disambiguate a job name shared across accounts.",
              "title": "User"
            },
            "description": "The run owner's email — needed only to disambiguate a job name shared across accounts."
          },
          {
            "name": "nodeRunId",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "minLength": 1
                },
                {
                  "type": "null"
                }
              ],
              "description": "Scope the ZIP to one node run — a `nodeRuns[].id` from `GET /runs/{id}`.",
              "title": "Noderunid"
            },
            "description": "Scope the ZIP to one node run — a `nodeRuns[].id` from `GET /runs/{id}`."
          },
          {
            "name": "runId",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "minLength": 1
                },
                {
                  "type": "null"
                }
              ],
              "description": "The run's id, needed only to disambiguate a job name you have reused across your own runs. It narrows the runs you can already see; a run id you cannot view is not found.",
              "title": "Runid"
            },
            "description": "The run's id, needed only to disambiguate a job name you have reused across your own runs. It narrows the runs you can already see; a run id you cannot view is not found."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicRunResults"
                }
              }
            }
          },
          "202": {
            "description": "The archive is still being built — GET again to keep waiting."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-doc-group": "Pipelines"
      }
    },
    "/api/pipelines/runs/{run_id}": {
      "get": {
        "tags": [
          "pipelines-public"
        ],
        "summary": "Get a pipeline run",
        "description": "Query a run for its status overall and per-node, along with output results.",
        "operationId": "getRun",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicRun"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-doc-group": "Pipelines"
      }
    },
    "/api/pipelines/runs/{run_id}/node-runs/{node_run_id}/molecules": {
      "get": {
        "tags": [
          "pipelines-public"
        ],
        "summary": "List the molecules a node run produced",
        "description": "The molecules one node run produced, with their scores.\n\nRead a node run's results through this, not through `PublicNodeRun.outputGroup`. `outputGroup` is the group a node MINTED, and two common kinds of node mint none: a node that enriches its inputs in place (scoring, structure prediction) leaves its molecules in the group they came from, and a filter node's survivors exist only as node-run outputs. For both, `outputGroup` is correctly `null` — reading results by group reports 'produced nothing' for exactly the node runs whose output you asked for. This endpoint answers for every kind of node.\n\n`node_run_id` is the `id` of an entry in the run's `nodeRuns`. `limit` is a maximum and a page may be shorter when rows are large; follow `nextCursor`.\n\nThis is the COMPACT view: identity, sequence and scores, scoped to the RUN. For the same molecules with their groups, tags and provenance — and to threshold or rank them by a score — pass the same id to `GET /molecules?nodeRunId=...`, which supports `filter`, `sortBy`, `search` and 200 per page. That endpoint reads through your molecule library rather than through the run, so its `nodeRunId` parameter documents where the two differ; this one is the authority on what the node run produced.",
        "operationId": "listNodeRunMolecules",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          },
          {
            "name": "node_run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Node Run Id"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 500,
              "minimum": 1,
              "description": "Molecules per page.",
              "default": 50,
              "title": "Limit"
            },
            "description": "Molecules per page."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 4096
                },
                {
                  "type": "null"
                }
              ],
              "description": "Pagination token from the previous response's `nextCursor`.",
              "title": "Cursor"
            },
            "description": "Pagination token from the previous response's `nextCursor`."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicNodeRunMoleculePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-doc-group": "Pipelines"
      }
    },
    "/api/pipelines/runs/{run_id}/stop": {
      "post": {
        "tags": [
          "pipelines-public"
        ],
        "summary": "Stop a pipeline run",
        "description": "Stop a pipeline run.\n\nAny jobs which have not yet completed are stopped, including running jobs.\nOutputs of any completed jobs are saved and may be viewed.",
        "operationId": "stopRun",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicRun"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-doc-group": "Pipelines"
      }
    },
    "/api/jobs": {
      "get": {
        "tags": [
          "jobs-public"
        ],
        "summary": "List jobs",
        "description": "List jobs using the established `/api/jobs` request and response contract, including exact-name, batch-child, organization, member, subjob, batch-only, and pagination modes.",
        "operationId": "listCompatibleJobs",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/LegacyJobPage"
                    },
                    {
                      "$ref": "#/components/schemas/LegacySingleJob"
                    },
                    {
                      "$ref": "#/components/schemas/LegacyMultiJob"
                    },
                    {
                      "$ref": "#/components/schemas/ProjectJobPage"
                    }
                  ],
                  "title": "Response Listcompatiblejobs"
                }
              }
            }
          },
          "400": {
            "description": "The request is valid HTTP but uses an unsupported parameter combination. With `projectId`: `{\"error\": \"...\"}` as `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicProblem"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectJobsError"
                }
              }
            }
          },
          "401": {
            "description": "API key missing or invalid. With `projectId`: `{\"error\": \"...\"}` as `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicProblem"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectJobsError"
                }
              }
            }
          },
          "403": {
            "description": "The authenticated API key is not allowed to use this operation. With `projectId`: `{\"error\": \"...\"}` as `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicProblem"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectJobsError"
                }
              }
            }
          },
          "404": {
            "description": "With `projectId`: the project does not exist, or you cannot see it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectJobsError"
                }
              }
            }
          },
          "429": {
            "description": "Another list request for this account is still running. Page sequentially: wait for it to finish, then retry after `Retry-After` seconds. With `projectId`: `{\"error\": \"...\"}` as `application/json`.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicProblem"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectJobsError"
                }
              }
            }
          },
          "503": {
            "description": "The operation could not be served. Read `retryable` in the response body before retrying: `true` means the same request can succeed later (honour `Retry-After` when present), `false` means it will not resolve on its own. When `retryable` is absent, treat the condition as transient and back off. With `projectId`: `{\"error\": \"...\"}` as `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicProblem"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectJobsError"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying, sent only when a retry is expected to help. Its ABSENCE on a 503 is meaningful — check `retryable` in the body.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "default": {
            "description": "An error occurred (RFC 9457 problem+json). Switch on the stable `code`, not on prose. With `projectId`: `{\"error\": \"...\"}` as `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicProblem"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectJobsError"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1000,
              "minimum": 1,
              "maximum": 1000
            },
            "description": "Most jobs to return on one page. A page can hold fewer, so use `startKey` to tell when you are done."
          },
          {
            "in": "query",
            "name": "startKey",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "The `startKey` from the previous response. Pass it back to get the next page; the last page has none."
          },
          {
            "in": "query",
            "name": "jobName",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            }
          },
          {
            "in": "query",
            "name": "includeSequences",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "in": "query",
            "name": "includeSubjobs",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "in": "query",
            "name": "organization",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "in": "query",
            "name": "batchOnly",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "in": "query",
            "name": "batch",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "A batch name. Returns one page of the batch's jobs; follow `startKey` for the rest."
          },
          {
            "in": "query",
            "name": "jobEmail",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            }
          },
          {
            "name": "projectId",
            "in": "query",
            "required": false,
            "description": "List the jobs filed under one project: its `ProjectId`, as returned by `GET /api/projects/list`. Newest first, paged with `limit` (default 1000) and `startKey` exactly like the plain listing, and answered in the list-page shape with `ProjectJobSummary` rows, plus `projectId` echoing the requested id: a response WITHOUT it did not filter by project. A pipeline run has `JobStatus: null` and a `pipelineRunId`; move it by sending that as `jobId` to `PUT /api/updateJobProject`. Cannot be combined with `jobName`, `batch`, `jobEmail`, `organization`, `batchOnly`, `includeSubjobs` or `includeSequences`, and repeating it with two different values is refused: both are a 400. A project that does not exist and one you cannot see are the same 404. With `projectId`, error bodies are `{\"error\": \"...\"}` rather than `application/problem+json`: a missing or invalid key is a 401 `{\"error\": \"Unauthorized\"}`; a browser session refused by an organization policy such as required MFA is a 403 `{\"error\": \"...\", \"code\": \"...\"}`; `cursor` is refused (page with `startKey`). An EMPTY or whitespace-only `projectId` is refused with a 400 in EITHER error shape (`{\"error\": \"...\"}` or the Jobs service's `application/problem+json`), so branch on the status, not the body; a misspelling such as `projectid` or `project_id` is a problem+json 400.",
            "schema": {
              "type": "string",
              "minLength": 1
            }
          }
        ]
      }
    },
    "/api/jobs/search": {
      "post": {
        "tags": [
          "jobs-public"
        ],
        "summary": "Look up many jobs by exact name",
        "description": "Resolve up to 1000 exact job names in ONE request and get each job's status back, plus the names that matched nothing in `notFound`. Exactly one row per resolved name, the newest — under `organization=true` a name can match once per member, and `GET /jobs?jobName=` is where every match is available. This is the batched form of `GET /jobs?jobName=`: polling or existence-checking a large campaign one name per request is a request volume that gets rate-limited and then blocked at the network edge. Names are sent in the body because a query string cannot hold a useful number of them. Rows carry status and timing but not `Settings`, `Score` or `AggregationError`, which have no size bound, or `resultUrl`, which costs a storage lookup per row. Read those for one job with `GET /jobs?jobName=`. To enumerate one batch's children instead of naming them, page `GET /jobs?batch=` with `limit` and `startKey`.",
        "operationId": "searchJobsByName",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LegacyJobNamesRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyJobNamesPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationProblem"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  },
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "servers": [
    {
      "url": "https://agent-prod-testing.preview.tamarind.bio",
      "description": "Tamarind API"
    }
  ],
  "tags": [
    {
      "name": "Jobs",
      "description": "Job submission and management"
    },
    {
      "name": "Tools",
      "description": "The tool catalogue"
    },
    {
      "name": "Files",
      "description": "File upload and management"
    },
    {
      "name": "Results",
      "description": "Job results retrieval"
    },
    {
      "name": "Models",
      "description": "Finetuning: the tools you can finetune (`/models`) and the models you have trained (`/finetuned-models`)"
    },
    {
      "name": "Usage",
      "description": "Usage statistics"
    },
    {
      "name": "Pipelines",
      "description": "Legacy saved pipelines"
    },
    {
      "name": "Assays",
      "description": "Assay Portal orders (Adaptyv, Ginkgo, A-Alpha Bio)"
    }
  ]
}