{
  "openapi": "3.0.3",
  "info": {
    "title": "TiDB",
    "version": "1.0.0",
    "description": "Search, read and write TiDB tables from a recipe, without writing SQL.\n\nWhat this adds over a plain MySQL connection:\n\n- **Semantic search.** Give it a sentence and it finds rows that mean the same thing, using\n  the vector and full-text indexes together. Expressing that in SQL takes two CTEs and a rank\n  fusion, which is not something a recipe builder should have to write.\n- **It knows which columns you must not fill in.** TiDB has columns the database owns:\n  generated columns (including auto-generated embeddings) reject an explicit value, and\n  AUTO_RANDOM primary keys are invisible to ordinary MySQL introspection, so a generic\n  connector shows them as writable and every insert from that recipe fails. This connector\n  classifies each column and hides the ones you cannot write.\n- **Approved queries.** Anything this connector cannot express is registered once by an\n  operator as a named, parameterized query and appears here as its own action. Recipes never\n  send SQL.\n\nScope of v1: one deployment serves one TiDB cluster and one database. There is no update, no\ndelete, and no schema creation. See the operation descriptions for what each one does not do.\n"
  },
  "servers": [
    {
      "url": "https://{host}/v1",
      "description": "Your TiDB connector deployment.",
      "variables": {
        "host": {
          "default": "workato-poc-mcp.fly.dev",
          "description": "Host of your deployment. Each deployment is bound to one TiDB cluster and one database by its own configuration; nothing about the cluster is sent from the recipe.\n"
        }
      }
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Connection",
      "description": "Connection health and capability. Not builder-facing."
    },
    {
      "name": "Discovery",
      "description": "Table metadata. Not builder-facing; drives field lists."
    },
    {
      "name": "Search",
      "description": "Semantic and keyword retrieval."
    },
    {
      "name": "Rows",
      "description": "Reading and writing rows."
    },
    {
      "name": "Approved queries",
      "description": "Named queries registered by an operator. The SQL is fixed; you supply parameters."
    }
  ],
  "paths": {
    "/ping": {
      "get": {
        "operationId": "ping",
        "summary": "Test connection",
        "tags": ["Connection"],
        "description": "Confirms the token is valid and the database is reachable, and reports which TiDB features this deployment can use. Used as the connection test. Never fails because the cluster is cold: if the database does not answer in time it still returns 200 with `warm: false` and `features` unknown, so a connection can be saved and the first real action warms it.\n",
        "responses": {
          "200": {
            "description": "Reachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PingResult"
                },
                "example": {
                  "ok": true,
                  "warm": true,
                  "database": "workato_poc",
                  "tidb_version": "8.0.11-TiDB-v8.5.3-serverless",
                  "features": {
                    "auto_embedding": true,
                    "fulltext_search": true,
                    "vector_index": true
                  },
                  "server_time_utc": "2026-08-05 09:00:00",
                  "latency_ms": 41,
                  "request_id": "01J9Z2K7Q1"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/tables": {
      "get": {
        "operationId": "listTables",
        "summary": "List tables",
        "tags": ["Discovery"],
        "description": "Tables in this deployment's database, with flags saying which ones semantic search will actually work against. Drives the table picker so a builder never types a table name.\n",
        "responses": {
          "200": {
            "description": "Tables.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TableList"
                },
                "example": {
                  "tables": [
                    {
                      "name": "tickets",
                      "approx_rows": 22,
                      "primary_key": ["ticket_id"],
                      "searchable": true,
                      "has_vector_column": true,
                      "has_fulltext_index": true
                    },
                    {
                      "name": "accounts",
                      "approx_rows": 5,
                      "primary_key": ["account_id"],
                      "searchable": false,
                      "has_vector_column": false,
                      "has_fulltext_index": false
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/tables/{table}": {
      "get": {
        "operationId": "describeTable",
        "summary": "Describe table",
        "tags": ["Discovery"],
        "description": "Columns of a table, each classified by whether a recipe may write to it.\n`write_class` is the point of this operation:\n- `writable`: fill it in normally. - `db_managed`: TiDB will fill it if you leave it out. Accepted if you send one, but the\n  value you send replaces the database's own, which is almost never what you want.\n  Hidden by default. Covers AUTO_INCREMENT keys and CURRENT_TIMESTAMP defaults.\n- `rejects_write`: TiDB refuses an explicit value, including an explicit null. Never\n  offered as an input. Covers generated columns (auto-generated embeddings) and\n  AUTO_RANDOM primary keys.\n\nAUTO_RANDOM is the case a generic MySQL connector gets wrong: it does not appear in `information_schema.COLUMNS.EXTRA`, so ordinary introspection renders it as a plain writable integer, a builder maps a value into it, and every insert fails.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/TablePath"
          }
        ],
        "responses": {
          "200": {
            "description": "Table schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TableSchema"
                },
                "example": {
                  "table": "tickets",
                  "primary_key": ["ticket_id"],
                  "unique_keys": [],
                  "schema_fingerprint": "93cedf20",
                  "search": {
                    "vector_columns": ["embedding"],
                    "default_vector_column": "embedding",
                    "fulltext_columns": ["body", "title"],
                    "has_vector_index": true,
                    "supports_hybrid": true
                  },
                  "write": {
                    "generated_key_column": null,
                    "reports_generated_key": "This table generates no key, so keys only ever echo what you sent."
                  },
                  "columns": [
                    {
                      "name": "ticket_id",
                      "type": "varchar(32)",
                      "json_type": "string",
                      "nullable": false,
                      "write_class": "writable",
                      "write_class_reason": null
                    },
                    {
                      "name": "title",
                      "type": "varchar(512)",
                      "json_type": "string",
                      "nullable": false,
                      "write_class": "writable",
                      "write_class_reason": null
                    },
                    {
                      "name": "created_at",
                      "type": "datetime",
                      "json_type": "string",
                      "nullable": false,
                      "write_class": "writable",
                      "write_class_reason": null
                    },
                    {
                      "name": "ingested_at",
                      "type": "datetime",
                      "json_type": "string",
                      "nullable": false,
                      "write_class": "db_managed",
                      "write_class_reason": "defaults to CURRENT_TIMESTAMP"
                    },
                    {
                      "name": "embedding",
                      "type": "vector(1024)",
                      "json_type": "array",
                      "nullable": true,
                      "write_class": "rejects_write",
                      "write_class_reason": "generated column, TiDB computes it from title and body",
                      "is_vector": true,
                      "vector_dims": 1024,
                      "embed_source_columns": ["title", "body", "resolution"]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/tables/{table}/search": {
      "post": {
        "operationId": "searchTable",
        "summary": "Search a table",
        "tags": ["Search"],
        "description": "Finds rows that mean the same thing as your text, not just rows containing the same words.\nRuns vector similarity and full-text matching together and fuses the two rankings, so a paraphrased question and an exact error code both land.\nEach side searches one column, chosen from the table's own indexes, so the common case needs no column names. A table carrying more than one embedding, or more than one full-text index, is the case where the default is only a default: name `vector_column` or `fulltext_column` to reach another. `GET /tables/{table}` lists every choice under `search`. The keyword side is one column and not several because TiDB rejects two full-text predicates in one statement.\nNot paginated. Ranked results past the top few are noise, so ask for what you will use.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/TablePath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SearchRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ranked matches, best first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResult"
                },
                "example": {
                  "matches": [
                    {
                      "relevance": 0.032258,
                      "row": {
                        "ticket_id": "SUP-1001",
                        "title": "Connection refused right after enabling the public endpoint",
                        "resolution": "Starter requires TLS on the public endpoint."
                      }
                    },
                    {
                      "relevance": 0.032018,
                      "row": {
                        "ticket_id": "SUP-1007",
                        "title": "Node.js client fails with self signed certificate in certificate chain",
                        "resolution": "The runtime image had no system CA bundle."
                      }
                    }
                  ],
                  "searched_with": "hybrid",
                  "query_time_ms": 118,
                  "request_id": "01J9Z2K7Q4"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/tables/{table}/upsert": {
      "post": {
        "operationId": "upsertRows",
        "summary": "Add or update rows",
        "tags": ["Rows"],
        "description": "Writes rows, replacing any that already exist with the same primary key. Send up to 5000 at a time rather than one row per recipe step.\nLeave out any column whose `write_class` is `rejects_write` or `db_managed`; TiDB fills those itself. Embeddings in particular are generated on write, so a recipe never sends a vector for a generated column.\nRows are not written as one transaction. Rows that succeed are committed even when others fail, and every row's outcome comes back in `results`. If nothing succeeded at all the call fails outright, so a completely broken batch cannot look like success.\nRetrying is safe only when the table's primary key comes from your data. For a table whose key is generated by TiDB, a retry inserts a second copy.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/TablePath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpsertRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "At least one row was written. Check `failed` before treating the step as clean.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteResult"
                },
                "example": {
                  "status": "ok",
                  "all_succeeded": true,
                  "succeeded": 2,
                  "failed": 0,
                  "results": [
                    {
                      "index": 0,
                      "ok": true,
                      "keys": {
                        "ticket_id": "SUP-9001"
                      }
                    },
                    {
                      "index": 1,
                      "ok": true,
                      "keys": {
                        "ticket_id": "SUP-9002"
                      }
                    }
                  ],
                  "request_id": "01J9Z2K7Q5"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "description": "Every row failed. Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteResult"
                },
                "example": {
                  "status": "failed",
                  "all_succeeded": false,
                  "succeeded": 0,
                  "failed": 1,
                  "results": [
                    {
                      "index": 0,
                      "ok": false,
                      "error": {
                        "code": "column_not_writable",
                        "message": "Column \"embedding\" is generated by TiDB and cannot be given a value. Remove it from the row.\n",
                        "column": "embedding"
                      }
                    }
                  ],
                  "request_id": "01J9Z2K7Q6"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/tables/{table}/list": {
      "post": {
        "operationId": "listRows",
        "summary": "List rows",
        "tags": ["Rows"],
        "description": "Reads rows with simple equality filters, in pages.\nPaging uses a cursor rather than a page number, so page 400 costs what page 1 costs. Hand the `cursor` from one response into the next call and stop when `has_more` is false. Rows written while you page may or may not appear; this is not a snapshot.\nVector columns are left out unless you ask for them by name in `columns`, because a single embedding is a thousand numbers and a page of them is not something a recipe can use.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/TablePath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A page of rows.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListResult"
                },
                "example": {
                  "rows": [
                    {
                      "ticket_id": "SUP-1102",
                      "account_id": "ACC-1002",
                      "status": "open",
                      "title": "Monthly report job failed again with error 8176",
                      "created_at": "2026-07-21 23:05:00"
                    }
                  ],
                  "has_more": false,
                  "cursor": null,
                  "returned": 1,
                  "clamped_by": null,
                  "schema_fingerprint": "93cedf20",
                  "request_id": "01J9Z2K7Q7"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/tables/{table}/count": {
      "post": {
        "operationId": "countRows",
        "summary": "Count rows",
        "tags": ["Rows"],
        "description": "How many rows match, without reading them. The question you ask right after a load finished: did all of them land.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/TablePath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CountRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The count.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CountResult"
                },
                "example": {
                  "count": 22,
                  "request_id": "01J9Z2K7Q8"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/q/search_known_issues": {
      "post": {
        "operationId": "searchKnownIssues",
        "summary": "Find similar resolved tickets",
        "tags": ["Approved queries"],
        "description": "Finds resolved support tickets similar to a customer's question, matching both meaning and exact tokens like error codes, and returns the fix along with the ticket it came from so a reply can cite its source.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["query_text", "product_filter", "top_k"],
                "properties": {
                  "query_text": {
                    "type": "string",
                    "description": "The customer's question, in their own words.",
                    "example": "our integration platform cannot complete the TLS handshake"
                  },
                  "product_filter": {
                    "type": "string",
                    "description": "Restrict to one product, or leave empty for all products.",
                    "example": ""
                  },
                  "top_k": {
                    "type": "integer",
                    "description": "How many to return. 3 to 5 works well.",
                    "minimum": 1,
                    "maximum": 20,
                    "example": 3
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Similar resolved tickets, best first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueryResult"
                },
                "example": {
                  "rows": [
                    {
                      "ticket_id": "SUP-1001",
                      "product": "TiDB Cloud Starter",
                      "title": "Connection refused right after enabling the public endpoint",
                      "resolution": "Starter requires TLS on the public endpoint.",
                      "resolved_on": "2026-02-11",
                      "relevance": 0.032258
                    }
                  ],
                  "returned": 1,
                  "request_id": "01J9Z2K7Q9"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/q/get_account_context": {
      "post": {
        "operationId": "getAccountContext",
        "summary": "Get account context",
        "tags": ["Approved queries"],
        "description": "Everything about an account before you reply: plan, entitlement, first-response SLA, how many tickets are open, how many times they contacted support in the last 30 days, and the durable facts remembered about their environment. One call, not four.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["account_id"],
                "properties": {
                  "account_id": {
                    "type": "string",
                    "example": "ACC-1002"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueryResult"
                },
                "example": {
                  "rows": [
                    {
                      "account_id": "ACC-1002",
                      "account_name": "Bluejay Analytics",
                      "plan": "pro",
                      "entitlement": "business-hours",
                      "sla_first_response_mins": 240,
                      "open_tickets": 2,
                      "contacts_30d": 3,
                      "remembered_facts": "workaround: splitting the scan by month is no longer holding"
                    }
                  ],
                  "returned": 1,
                  "request_id": "01J9Z2K7QA"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/q/check_repeat_contact": {
      "post": {
        "operationId": "checkRepeatContact",
        "summary": "Check repeat contact",
        "tags": ["Approved queries"],
        "description": "How many tickets this account opened recently, with the ticket ids. Use this to decide escalation instead of judging it: the count is a fact you can quote and an escalation that cites ticket ids can be audited.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["account_id", "days"],
                "properties": {
                  "account_id": {
                    "type": "string",
                    "example": "ACC-1002"
                  },
                  "days": {
                    "type": "integer",
                    "description": "Window size in days. Use 30 unless told otherwise.",
                    "minimum": 1,
                    "maximum": 365,
                    "example": 30
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contact counts for the window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueryResult"
                },
                "example": {
                  "rows": [
                    {
                      "contacts_in_window": 3,
                      "still_open": 2,
                      "ticket_ids": "SUP-1014,SUP-1102,SUP-1103",
                      "first_contact": "2026-07-10 16:20:00",
                      "latest_contact": "2026-07-28 15:40:00"
                    }
                  ],
                  "returned": 1,
                  "request_id": "01J9Z2K7QB"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/q/record_case": {
      "post": {
        "operationId": "recordCase",
        "summary": "Record a resolution",
        "tags": ["Approved queries"],
        "description": "Writes a resolution back onto a ticket so the next similar question retrieves this answer. The stored embedding is recomputed automatically, so the ticket starts matching questions it could not answer before, with no reindexing step.\nCall it only once the issue is actually resolved: it stamps the resolution time on every call, so calling twice moves that timestamp.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["ticket_id", "resolution", "status"],
                "properties": {
                  "ticket_id": {
                    "type": "string",
                    "example": "SUP-1101"
                  },
                  "resolution": {
                    "type": "string",
                    "description": "What actually fixed it, written so a future reader can act on it.",
                    "example": "Supply the ISRG Root X1 CA in the connector's SSL settings."
                  },
                  "status": {
                    "type": "string",
                    "enum": ["resolved", "escalated"],
                    "example": "resolved"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteQueryResult"
                },
                "example": {
                  "affected_rows": 1,
                  "applied": true,
                  "request_id": "01J9Z2K7QC"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A token issued by whoever runs this deployment. It is not a TiDB credential: the database user and password stay in the deployment's own configuration and are never sent from a recipe.\n"
      }
    },
    "parameters": {
      "TablePath": {
        "name": "table",
        "in": "path",
        "required": true,
        "description": "Table name. Pick from List tables.",
        "schema": {
          "type": "string",
          "example": "tickets"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request was malformed or named something the table does not have: an unknown column in a filter, in `columns` or in `order_by`, a cursor that is unreadable or was issued for a different sort, or `updated_since` on a table whose timestamp column cannot be worked out. The `column` field names the offender where there is one.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "column_not_found",
                "message": "Table 'tickets' has no column 'prioirty'.",
                "column": "prioirty",
                "retryable": false
              },
              "request_id": "3f9c1a77-8e42-4d1b-9a05-6c7e2b4d8f10"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The request body exceeded the size cap, or a write carried more rows than one call accepts. Split the batch; the caps are on the whole surface, not on one operation.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "too_many_rows",
                "message": "At most 5000 rows per call; received 8000.",
                "retryable": false
              },
              "request_id": "91b4c0de-7a13-4f52-8c66-1d2e9f3a5b78"
            }
          }
        }
      },
      "InternalError": {
        "description": "Something failed that this connector does not recognize. The cause is deliberately not in this response: it can carry index names, column names and fragments of the statement. Quote `request_id` to an operator, who can find the one log line that has it.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "internal_error",
                "message": "The request could not be completed.",
                "retryable": false
              },
              "request_id": "c4a7e219-05bd-4e83-b71f-8a6d3c1092ef"
            }
          }
        }
      },
      "NotConfigured": {
        "description": "This deployment has no database configured. A configuration error rather than a request error, so it is the same answer for every call until an operator fixes it. Retrying will not help.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "not_configured",
                "message": "This deployment has no database configuration.",
                "retryable": false
              },
              "request_id": "7d2f8b40-1c96-4a5e-9038-e5b1a7c46d23"
            }
          }
        }
      },
      "GatewayTimeout": {
        "description": "The statement did not finish inside the server's own limit and was cut off rather than left to outlive the caller. Narrow the filter, lower the limit, or ask for fewer rows.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "query_timeout",
                "message": "Statement did not finish within 25000 ms.",
                "retryable": false
              },
              "request_id": "2e6b9d31-4f70-4c18-a25d-b90c7f83e614"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests in the interval. Every operation can answer this: the limit is applied to the whole surface and before authentication, so an unauthenticated flood is bounded too. `Retry-After` gives the seconds to wait, and `retryable` is true.\n",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the window resets.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "Too many requests. Retry after the interval given.",
                "retryable": true
              },
              "request_id": "6f1b9a2c-4d3e-4a71-9f0c-2b8e5d7a1c34"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid token.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "unauthorized",
                "message": "Token missing or not recognized.",
                "retryable": false
              },
              "request_id": "01J9Z2K7QD"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such table.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "table_not_found",
                "message": "No table named 'tikets' in workato_poc.",
                "retryable": false
              },
              "request_id": "01J9Z2K7QE"
            }
          }
        }
      },
      "Unprocessable": {
        "description": "The request was understood but cannot be run as written.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "feature_unavailable",
                "message": "Semantic search needs a vector index. Table 'accounts' has none, so only exact filters are available on it.\n",
                "retryable": false
              },
              "request_id": "01J9Z2K7QF"
            }
          }
        }
      }
    },
    "schemas": {
      "Filter": {
        "type": "object",
        "description": "One equality condition. Conditions are combined with AND.",
        "required": ["column", "value"],
        "properties": {
          "column": {
            "type": "string",
            "example": "status"
          },
          "value": {
            "description": "Matched exactly. Send it as it is stored: text as text, whole numbers as numbers, dates as \"YYYY-MM-DD HH:MM:SS\" with no timezone suffix.\n",
            "example": "open"
          }
        },
        "additionalProperties": false
      },
      "Row": {
        "type": "object",
        "description": "One row, keyed by column name. Missing values come back as null rather than being left out, so a recipe field never silently disappears when a value is empty.\n",
        "additionalProperties": true
      },
      "ErrorEnvelope": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable reason. One of: unauthorized, table_not_found, column_not_found, column_not_writable, duplicate_key, write_conflict, schema_changed, feature_unavailable, query_timeout, rate_limited, internal_error.\n",
                "example": "column_not_writable"
              },
              "message": {
                "type": "string",
                "description": "Plain description safe to show a builder."
              },
              "column": {
                "type": "string",
                "nullable": true
              },
              "retryable": {
                "type": "boolean",
                "description": "True only for transient causes such as a write conflict or rate limiting."
              }
            }
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "PingResult": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "warm": {
            "type": "boolean",
            "description": "False when the cluster did not answer in time and the database facts below are unknown. The connection is still valid; the first real action will warm it.\n"
          },
          "database": {
            "type": "string",
            "nullable": true
          },
          "tidb_version": {
            "type": "string",
            "nullable": true
          },
          "features": {
            "type": "object",
            "description": "What this cluster can do. Auto embedding and full-text search are available on TiDB Cloud Starter on AWS; elsewhere search falls back to what the table's indexes support.\n",
            "properties": {
              "auto_embedding": {
                "type": "boolean",
                "nullable": true
              },
              "fulltext_search": {
                "type": "boolean",
                "nullable": true
              },
              "vector_index": {
                "type": "boolean",
                "nullable": true
              }
            }
          },
          "server_time_utc": {
            "type": "string"
          },
          "latency_ms": {
            "type": "integer"
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "TableList": {
        "type": "object",
        "properties": {
          "tables": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "approx_rows": {
                  "type": "integer"
                },
                "primary_key": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "searchable": {
                  "type": "boolean",
                  "description": "True when Search will work on this table, that is it has a vector column or a full-text index."
                },
                "has_vector_column": {
                  "type": "boolean"
                },
                "has_fulltext_index": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      },
      "TableSchema": {
        "type": "object",
        "properties": {
          "table": {
            "type": "string"
          },
          "primary_key": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "unique_keys": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          "schema_fingerprint": {
            "type": "string",
            "description": "Changes when the table's shape changes. Informational only, never enforced: a recipe can compare it across runs to notice a schema change instead of finding out through blank fields.\n"
          },
          "search": {
            "type": "object",
            "description": "What this table can be searched on, and what Search will pick if you say nothing. This is the list `vector_column` and `fulltext_column` are chosen from, which is what makes them discoverable rather than guessable.\n",
            "properties": {
              "vector_columns": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Every embedding column on the table, indexed or not."
              },
              "default_vector_column": {
                "type": "string",
                "nullable": true,
                "description": "The one Search uses unless told otherwise: the first vector column carrying a vector index. Null means searching by meaning is unavailable here.\n"
              },
              "fulltext_columns": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Every full-text indexed column. Search uses the first unless told otherwise, and only ever one of them.\n"
              },
              "has_vector_index": {
                "type": "boolean",
                "description": "Whether an index backs the vector column. False still searches, by scanning, which is fine on a small table and not on a large one.\n"
              },
              "supports_hybrid": {
                "type": "boolean",
                "description": "Whether the two rankings can be fused here. Requires an embedding, a full-text index, and a single-column primary key to join the two ranked sets on. A composite primary key is the case that fails, and it fails with a named reason rather than by degrading to one side.\n"
              }
            }
          },
          "write": {
            "type": "object",
            "description": "What a write to this table will report back. Here so the rule is discoverable rather than inferred from an empty field: a caller who sends two rows and gets no key cannot otherwise tell that apart from a table that generates no key at all.\n",
            "properties": {
              "generated_key_column": {
                "type": "string",
                "nullable": true,
                "description": "The column the database fills in itself (AUTO_RANDOM or AUTO_INCREMENT), or null when there is none. Only this column is ever filled in from a generated value; on a composite key the other parts report null unless you sent them.\n"
              },
              "reports_generated_key": {
                "type": "string",
                "description": "When that key comes back. Today, only for a request carrying exactly one row.\n"
              }
            }
          },
          "columns": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "type": {
                  "type": "string",
                  "description": "The TiDB column type."
                },
                "json_type": {
                  "type": "string",
                  "enum": ["string", "number", "boolean", "array", "object"],
                  "description": "How the value appears in JSON. Whole-number keys arrive as strings because they can exceed what JSON numbers represent exactly; counts and scores arrive as numbers so they can be compared directly.\n"
                },
                "nullable": {
                  "type": "boolean"
                },
                "write_class": {
                  "type": "string",
                  "enum": ["writable", "db_managed", "rejects_write"]
                },
                "write_class_reason": {
                  "type": "string",
                  "nullable": true
                },
                "is_vector": {
                  "type": "boolean"
                },
                "vector_dims": {
                  "type": "integer",
                  "nullable": true
                },
                "embed_source_columns": {
                  "type": "array",
                  "nullable": true,
                  "items": {
                    "type": "string"
                  },
                  "description": "Columns TiDB reads to build this embedding. Changing one recomputes it."
                }
              }
            }
          }
        }
      },
      "SearchRequest": {
        "type": "object",
        "required": ["query_text"],
        "properties": {
          "query_text": {
            "type": "string",
            "description": "What you are looking for, in ordinary words.",
            "example": "customer cannot connect, the TLS handshake fails"
          },
          "top_k": {
            "type": "integer",
            "default": 5,
            "minimum": 1,
            "maximum": 50,
            "description": "How many matches to return."
          },
          "filters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Filter"
            },
            "description": "Narrow the search before ranking. Combined with AND."
          },
          "columns": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Which columns to return. Leave empty for all except vectors."
          },
          "mode": {
            "type": "string",
            "enum": ["auto", "hybrid", "meaning", "keyword"],
            "default": "auto",
            "description": "Leave on auto. It uses meaning and keywords together when the table supports both, and falls back to whichever it has.\n"
          },
          "vector_column": {
            "type": "string",
            "nullable": true,
            "description": "Which embedding to search by meaning. Defaults to the table's first indexed vector column, which is the only one on most tables. Naming a column the table does not carry an embedding on is an error, not a silent fallback. Choices are in the `search` block of GET /tables/{table}.\n",
            "example": "embedding"
          },
          "fulltext_column": {
            "type": "string",
            "nullable": true,
            "description": "Which column to match by keyword. Defaults to the table's first full-text indexed column. One column, not several: TiDB will not accept two full-text predicates in one statement. Choices are in the `search` block of GET /tables/{table}.\n",
            "example": "body"
          }
        },
        "additionalProperties": false
      },
      "SearchResult": {
        "type": "object",
        "properties": {
          "matches": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "relevance": {
                  "type": "number",
                  "description": "Higher is a better match. Comparable within one response, not across responses."
                },
                "row": {
                  "$ref": "#/components/schemas/Row"
                }
              }
            }
          },
          "searched_with": {
            "type": "string",
            "enum": ["hybrid", "meaning", "keyword"],
            "description": "What actually ran, so a keyword-only fallback is visible rather than silent."
          },
          "query_time_ms": {
            "type": "integer"
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "UpsertRequest": {
        "type": "object",
        "required": ["rows"],
        "properties": {
          "rows": {
            "type": "array",
            "minItems": 1,
            "maxItems": 5000,
            "items": {
              "$ref": "#/components/schemas/Row"
            },
            "description": "The rows to write. Include only columns whose write_class is writable.\n"
          }
        },
        "additionalProperties": false
      },
      "WriteResult": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": ["ok", "partial", "failed"]
          },
          "all_succeeded": {
            "type": "boolean",
            "description": "Branch on this. False means read `results` to see which rows did not land."
          },
          "succeeded": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer",
                  "description": "Position in the rows you sent."
                },
                "ok": {
                  "type": "boolean"
                },
                "keys": {
                  "type": "object",
                  "additionalProperties": true,
                  "nullable": true,
                  "description": "The row's primary key after writing. A key you sent is echoed back. A key the database generated (AUTO_RANDOM, AUTO_INCREMENT) is reported only when the request contained exactly ONE row; send one row per call when you need the key back. A multi-row call reports null for generated keys, because one statement reports one key however many rows it wrote and there is no way to tell which row it belongs to.\nGenerated keys arrive as strings. They routinely exceed what a JSON number holds exactly, so comparing one numerically will not do what you expect; treat it as an opaque identifier.\nOn a composite key, only the column the database actually generates is filled in. The others report null unless you sent them.\n"
                },
                "error": {
                  "nullable": true,
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "column": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "ListRequest": {
        "type": "object",
        "properties": {
          "filters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Filter"
            }
          },
          "updated_since": {
            "type": "string",
            "nullable": true,
            "description": "Only rows whose timestamp column is at or after this. Format \"YYYY-MM-DD HH:MM:SS\", no timezone suffix. Used as a starting bound, never as the paging key, so rows sharing a timestamp are not skipped at a page boundary.\n",
            "example": "2026-07-01 00:00:00"
          },
          "updated_since_column": {
            "type": "string",
            "nullable": true,
            "description": "Which column `updated_since` bounds. Left empty, the column is guessed: `updated_at`, then any datetime column whose name ends in `_at`. A table where the guess finds nothing is a 400 naming this parameter, never a call that quietly returns every row, because an incremental sync that silently receives the whole table has no way to notice. Ignored when `updated_since` is empty.\n",
            "example": "updated_at"
          },
          "columns": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Which columns to return. Leave empty for all except vectors."
          },
          "order_by": {
            "type": "string",
            "nullable": true,
            "description": "Column to sort by. Defaults to the primary key."
          },
          "limit": {
            "type": "integer",
            "default": 500,
            "minimum": 1,
            "maximum": 10000
          },
          "cursor": {
            "type": "string",
            "nullable": true,
            "description": "Leave empty for the first page, then pass the cursor from the previous response."
          }
        },
        "additionalProperties": false
      },
      "ListResult": {
        "type": "object",
        "properties": {
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Row"
            }
          },
          "returned": {
            "type": "integer"
          },
          "has_more": {
            "type": "boolean",
            "description": "Call again with `cursor` while this is true."
          },
          "cursor": {
            "type": "string",
            "nullable": true
          },
          "clamped_by": {
            "type": "string",
            "nullable": true,
            "enum": [null, "rows", "bytes"],
            "description": "Set when the page was cut short. `bytes` means the rows were large and fewer were returned than you asked for. `has_more` is true in that case; it is never a silent truncation.\n"
          },
          "schema_fingerprint": {
            "type": "string"
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "CountRequest": {
        "type": "object",
        "properties": {
          "filters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Filter"
            }
          }
        },
        "additionalProperties": false
      },
      "CountResult": {
        "type": "object",
        "properties": {
          "count": {
            "type": "integer",
            "description": "A number, not a string, so it can be compared directly in a recipe."
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "QueryResult": {
        "type": "object",
        "properties": {
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Row"
            }
          },
          "returned": {
            "type": "integer"
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "WriteQueryResult": {
        "type": "object",
        "properties": {
          "affected_rows": {
            "type": "integer",
            "description": "Rows the statement changed."
          },
          "applied": {
            "type": "boolean",
            "description": "Branch on this, not on the count. False means nothing was written, and it is the answer both when no row matched the parameters and when the row already held these values. A bare zero reads as success to anything summarising the call, which is the mistake this field exists to stop.\n"
          },
          "note": {
            "type": "string",
            "description": "Present only when `applied` is false. Says which two situations produced it, because telling them apart would need a read before the write.\n"
          },
          "request_id": {
            "type": "string"
          }
        }
      }
    }
  }
}
