> For the complete documentation index, see [llms.txt](https://docs.zero.inc/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.zero.inc/developers/api/columns.md).

# Columns

Manage custom property definitions (called *columns* in the API) for companies, contacts, and deals. Use `GET /api/columns` to discover the IDs, types, and options of custom fields before filtering or updating records, and `POST` / `PATCH` / `DELETE` to create, edit, and remove properties.

#### Definitions vs. values

A **column** is the *definition* of a custom property — its name, type, options, and which entity it belongs to. The *values* live on the records themselves, in the `custom` object of each company / contact / deal, keyed by the column's `id`. Creating a column does not touch any records; it just makes `custom.<column-id>` available for writes, filters, views, and reports.

```
Column                              Company record
{ "id": "54e1ca7d-…",               { "id": "…",
  "entity": "companies",              "name": "Acme",
  "name": "Industry",                 "custom": {
  "type": "select",                     "54e1ca7d-…": "3e839b5c-…"   ← option key
  "options": [ {"key": "3e839b5c-…", "name": "SaaS"}, … ] }   }
}
```

#### Creating a property

`POST /api/columns` with at least `workspaceId`, `entity`, `name`, and `type`. For `select` / `multiselect`, also pass `options` — every option needs a **client-generated UUID** in `key`; that UUID is what gets stored on records, so it must stay stable for the life of the option. Names and colors can be edited freely later.

#### Editing a property

`PATCH /api/columns/{columnId}`. Renaming, changing the description, and editing options are safe. Note that `options` is **replaced wholesale** on write — always send the complete list. Removing an option from `options` does not delete it from records; move it to `archivedOptions` instead so existing values keep resolving to a name. Changing `type` is allowed for a limited set of pairs and triggers a background migration of existing values — see `PATCH` for the supported conversions.

#### Deleting a property

`DELETE /api/columns/{columnId}` permanently removes the definition. Values already stored under that id on records are **not** cleaned up — they simply become invisible/orphaned. Prefer `DELETE …?archive=true` (soft delete) when you may need to restore the property; restore with `PATCH {"archived": false}`.

#### Reserved / built-in fields

Only user-defined properties are columns. Built-in fields (`name`, `domain`, `stage`, `value`, …) are top-level record fields, not columns, and cannot be created or deleted through this endpoint.

## List custom property definitions

> Returns the custom property (column) definitions for a workspace. Filter by workspace using the \`where\` parameter: \`{"workspaceId": "\<WORKSPACE\_UUID>"}\`. Strongly recommended — omitting it returns columns across all workspaces the user has access to.\
> \
> Use this endpoint to discover the IDs, types, and available options of custom fields before filtering or updating records.\
> \
> Custom properties are stored in the \`custom\` object on companies, contacts, and deals. Each custom property is keyed by its UUID (\`id\` from this response).\
> \
> For example, if this endpoint returns a column with \`"id": "54e1ca7d-69c3-4b77-8266-8085b5834116"\` and \`"type": "select"\`, you can:\
> \- \*\*Filter\*\* records using: \`{"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": {"$in": \["\<OPTION\_KEY\_UUID>", "\<OPTION\_KEY\_UUID>"]}}\`\
> \- \*\*Update\*\* a record using: \`{"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": "\<OPTION\_KEY\_UUID>"}\`\
> \
> \> \*\*Note:\*\* For \`select\` and \`multiselect\` columns, option values are UUIDs stored in the \`key\` field of each option object. Use the \`key\` UUID (not the human-readable \`name\`) when filtering or updating records.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Columns","description":"Manage custom property definitions (called *columns* in the API) for companies, contacts, and deals. Use `GET /api/columns` to discover the IDs, types, and options of custom fields before filtering or updating records, and `POST` / `PATCH` / `DELETE` to create, edit, and remove properties.\n\n### Definitions vs. values\nA **column** is the *definition* of a custom property — its name, type, options, and which entity it belongs to. The *values* live on the records themselves, in the `custom` object of each company / contact / deal, keyed by the column's `id`. Creating a column does not touch any records; it just makes `custom.<column-id>` available for writes, filters, views, and reports.\n\n```\nColumn                              Company record\n{ \"id\": \"54e1ca7d-…\",               { \"id\": \"…\",\n  \"entity\": \"companies\",              \"name\": \"Acme\",\n  \"name\": \"Industry\",                 \"custom\": {\n  \"type\": \"select\",                     \"54e1ca7d-…\": \"3e839b5c-…\"   ← option key\n  \"options\": [ {\"key\": \"3e839b5c-…\", \"name\": \"SaaS\"}, … ] }   }\n}\n```\n\n### Creating a property\n`POST /api/columns` with at least `workspaceId`, `entity`, `name`, and `type`. For `select` / `multiselect`, also pass `options` — every option needs a **client-generated UUID** in `key`; that UUID is what gets stored on records, so it must stay stable for the life of the option. Names and colors can be edited freely later.\n\n### Editing a property\n`PATCH /api/columns/{columnId}`. Renaming, changing the description, and editing options are safe. Note that `options` is **replaced wholesale** on write — always send the complete list. Removing an option from `options` does not delete it from records; move it to `archivedOptions` instead so existing values keep resolving to a name. Changing `type` is allowed for a limited set of pairs and triggers a background migration of existing values — see `PATCH` for the supported conversions.\n\n### Deleting a property\n`DELETE /api/columns/{columnId}` permanently removes the definition. Values already stored under that id on records are **not** cleaned up — they simply become invisible/orphaned. Prefer `DELETE …?archive=true` (soft delete) when you may need to restore the property; restore with `PATCH {\"archived\": false}`.\n\n### Reserved / built-in fields\nOnly user-defined properties are columns. Built-in fields (`name`, `domain`, `stage`, `value`, …) are top-level record fields, not columns, and cannot be created or deleted through this endpoint.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}},"where":{"name":"where","in":"query","description":"JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.\n\nThe available operators depend on the **data type** of the field you are filtering on.\n\n---\n\n## String fields\nFields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"name\": \"Linear\"}` |\n| `$eq` | Explicit exact match | `{\"name\": {\"$eq\": \"Linear\"}}` |\n| `$not` | Not equal | `{\"source\": {\"$not\": \"import\"}}` |\n| `$in` | Matches any value in array | `{\"domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}` |\n| `$notIn` | Matches none of the values | `{\"source\": {\"$notIn\": [\"import\", \"api\"]}}` |\n| `$contains` | Case-insensitive word-boundary substring match | `{\"name\": {\"$contains\": \"YC\"}}` |\n| `$notContains` | Does not contain | `{\"name\": {\"$notContains\": \"Test\"}}` |\n| `$containsAny` | Contains any of the given strings | `{\"name\": {\"$containsAny\": [\"YC\", \"Techstars\"]}}` |\n| `$startsWith` | Starts with prefix | `{\"domain\": {\"$startsWith\": \"app.\"}}` |\n| `$endsWith` | Ends with suffix | `{\"email\": {\"$endsWith\": \"@zero.inc\"}}` |\n| `$exists` | Field is present and truthy | `{\"linkedin\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or empty | `{\"linkedin\": {\"$notExists\": true}}` |\n\n---\n\n## Number fields\nFields like `value`, `confidence`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"value\": 5000}` |\n| `$eq` | Explicit exact match | `{\"value\": {\"$eq\": 5000}}` |\n| `$not` | Not equal | `{\"value\": {\"$not\": 0}}` |\n| `$gt` | Greater than | `{\"value\": {\"$gt\": 10000}}` |\n| `$gte` | Greater than or equal | `{\"value\": {\"$gte\": 5000}}` |\n| `$lt` | Less than | `{\"value\": {\"$lt\": 10000}}` |\n| `$lte` | Less than or equal | `{\"value\": {\"$lte\": 50000}}` |\n| `$in` | Matches any value in array | `{\"confidence\": {\"$in\": [0.25, 0.5, 0.75]}}` |\n| `$notIn` | Matches none of the values | `{\"confidence\": {\"$notIn\": [0, 1]}}` |\n| `$exists` | Field is present and truthy | `{\"value\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent, null, or zero | `{\"value\": {\"$notExists\": true}}` |\n\nMultiple operators can be combined on one field:\n```json\n{\"value\": {\"$gte\": 5000, \"$lt\": 10000}}\n```\n\n---\n\n## Date fields\nFields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.\n\nValues can be ISO 8601 strings (`\"2026-01-01\"`, `\"2026-01-01T00:00:00Z\"`) or **relative time macros**.\n\n**Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$gte` | On or after | `{\"closeDate\": {\"$gte\": \"2026-01-01\"}}` |\n| `$lte` | On or before | `{\"closeDate\": {\"$lte\": \"2026-03-31\"}}` |\n| `$gt` | After | `{\"createdAt\": {\"$gt\": \"2026-01-01T00:00:00Z\"}}` |\n| `$lt` | Before | `{\"createdAt\": {\"$lt\": \"now()\"}}` |\n| `$date` | Exact date match (compares date portion only) | `{\"closeDate\": {\"$date\": \"2026-01-15\"}}` |\n| `$exists` | Field is present and truthy | `{\"closeDate\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or null | `{\"closeDate\": {\"$notExists\": true}}` |\n\nDate range example:\n```json\n{\"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"}}\n```\nRelative date example (closing in next 30 days):\n```json\n{\"closeDate\": {\"$gte\": \"now()\", \"$lte\": \"+30d\"}}\n```\n\n---\n\n## Array fields\nFields like `listIds`, `ownerIds`, `contactIds`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{\"listIds\": {\"$includes\": \"<LIST_UUID>\"}}` |\n| `$notIncludes` | Array does not contain the given value | `{\"ownerIds\": {\"$notIncludes\": \"<USER_UUID>\"}}` |\n| `$overlaps` | Array contains at least one of the given values | `{\"ownerIds\": {\"$overlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notOverlaps` | Array contains none of the given values | `{\"listIds\": {\"$notOverlaps\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$all` | Array contains all of the given values | `{\"listIds\": {\"$all\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$length` | Filter by array length (supports nested operators) | `{\"contactIds\": {\"$length\": {\"$gte\": 2}}}` |\n| `$exists` | Field is present and non-empty | `{\"ownerIds\": {\"$exists\": true}}` |\n| `$notExists` | Field is absent or empty | `{\"ownerIds\": {\"$notExists\": true}}` |\n\n---\n\n## UUID / ID fields\nFields like `id`, `workspaceId`, `companyId`, `stage`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"stage\": \"<PIPELINE_STAGE_UUID>\"}` |\n| `$in` | Matches any value in array | `{\"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]}}` |\n| `$notIn` | Matches none of the values | `{\"stage\": {\"$notIn\": [\"<UUID_1>\"]}}` |\n| `$not` | Not equal | `{\"stage\": {\"$not\": \"<UUID>\"}}` |\n\n---\n\n## Boolean fields\nFields like `archived`.\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| *(exact)* | Exact match | `{\"archived\": false}` |\n| `$not` | Not equal | `{\"archived\": {\"$not\": true}}` |\n\n---\n\n## Logical operators\nThese work across all data types.\n\n`$or` — matches if **any** sub-condition is true:\n```json\n{\n  \"workspaceId\": \"<WORKSPACE_UUID>\",\n  \"$or\": [\n    {\"ownerIds\": {\"$overlaps\": [\"<USER_UUID_1>\", \"<USER_UUID_2>\"]}},\n    {\"closeDate\": {\"$gte\": \"2026-01-01\"}}\n  ]\n}\n```\n\n`$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):\n```json\n{\n  \"$and\": [\n    {\"name\": {\"$contains\": \"Enterprise\"}},\n    {\"name\": {\"$notContains\": \"Test\"}}\n  ]\n}\n```\n\n---\n\n## Dot-notation (relation filtering)\nUse dot-syntax to filter records based on properties of related objects:\n```json\n{\"company.name\": \"Linear\"}\n{\"company.domain\": {\"$in\": [\"linear.app\", \"granola.so\"]}}\n{\"company.location.city\": \"San Francisco\"}\n{\"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]}}\n```\nAll operators available for the target field's data type can be used with dot-notation.\n\n---\n\n## Custom property filtering\nCustom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.\n\nFilter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.\n\n```json\n// Text custom property\n{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}}\n\n// Select custom property (use the option key UUID)\n{\"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}\n\n// Multi-select custom property (use option key UUIDs)\n{\"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901\": {\"$overlaps\": [\"a1b1c1d1-e1f1-1111-aaaa-111111111111\", \"b2b2c2d2-e2f2-2222-bbbb-222222222222\"]}}\n\n// Currency/number custom property\n{\"custom.c3d4e5f6-a7b8-9012-cdef-123456789012\": {\"$gte\": 100000}}\n\n// Date custom property\n{\"custom.d4e5f6a7-b8c9-0123-defa-234567890123\": {\"$gte\": \"2026-01-01\"}}\n\n// Boolean custom property\n{\"custom.e5f6a7b8-c9d0-1234-efab-345678901234\": true}\n\n// Check if custom property has a value\n{\"custom.f6a7b8c9-d0e1-2345-fabc-456789012345\": {\"$exists\": true}}\n```\n\n---\n\n## Complex example\nAll top-level keys are ANDed together:\n```json\n{\n  \"name\": {\"$contains\": \"Zero\"},\n  \"location.city\": \"Helsinki\",\n  \"location.country\": {\"$in\": [\"United Kingdom\", \"Germany\", \"Sweden\"]},\n  \"value\": {\"$gte\": 10000},\n  \"closeDate\": {\"$gte\": \"2026-01-01\", \"$lte\": \"2026-03-31\"},\n  \"ownerIds\": {\"$includes\": \"<USER_UUID>\"},\n  \"stage\": {\"$in\": [\"<UUID_1>\", \"<UUID_2>\"]},\n  \"lastActivity\": {\"$exists\": true},\n  \"companyProfile.categories\": {\"$overlaps\": [\"Sales\", \"Marketing\"]},\n  \"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$contains\": \"enterprise\"}\n}\n```\n","schema":{"type":"string"}},"limit":{"name":"limit","in":"query","description":"Maximum number of records to return","schema":{"type":"integer","default":100}},"offset":{"name":"offset","in":"query","description":"Pagination offset","schema":{"type":"integer","default":0}},"orderBy":{"name":"orderBy","in":"query","description":"JSON string for sort order","schema":{"type":"string"}}},"schemas":{"Column":{"type":"object","description":"A custom property definition. The `id` is used as the key inside the `custom` object on records (`custom.<id>`).","properties":{"id":{"type":"string","format":"uuid","description":"The unique identifier for this custom property. Use this as the key when filtering or updating custom fields on records."},"workspaceId":{"type":"string","format":"uuid"},"entity":{"type":"string","enum":["companies","contacts","deals"],"description":"Which record type this custom property belongs to (plural form). Fixed at creation."},"name":{"type":"string","description":"Human-readable name of the custom property"},"description":{"type":"string","nullable":true,"description":"Optional description of the custom property, shown as help text in the app"},"key":{"type":"string","nullable":true,"description":"Legacy/optional field, usually `null`. Do not rely on it — the path to use in filters and record updates is always `custom.<id>`."},"type":{"type":"string","enum":["text","richtext","number","percentage","currency","boolean","link","email","phone","select","multiselect","user","users","date","timestamp","location","rating","company","companies","contact","contacts","deal","deals"],"description":"The data type of the custom property. This determines the shape of the value stored at `custom.<id>` on records:\n\n| Type | Value stored on records |\n|------|-------------------------|\n| `text` | Plain string |\n| `richtext` | Rich-text string / formatted content |\n| `number` | Number |\n| `percentage` | Number (e.g. `12.5` for 12.5%) |\n| `currency` | Number, displayed in the workspace currency |\n| `boolean` | `true` / `false` |\n| `link` | URL string |\n| `email` | Email address string |\n| `phone` | Phone number string |\n| `select` | One option `key` UUID from `options` |\n| `multiselect` | Array of option `key` UUIDs from `options` |\n| `user` | One workspace user UUID |\n| `users` | Array of workspace user UUIDs |\n| `date` | `YYYY-MM-DD` string |\n| `timestamp` | ISO-8601 date-time string |\n| `location` | Object with any of `city`, `state`, `country`, `continent` (e.g. `{\"city\": \"Berlin\", \"country\": \"Germany\"}`) |\n| `rating` | Integer `1`–`5` |\n| `company` | One company UUID |\n| `companies` | Array of company UUIDs |\n| `contact` | One contact UUID |\n| `contacts` | Array of contact UUIDs |\n| `deal` | One deal UUID |\n| `deals` | Array of deal UUIDs |\n\nOther type values exist internally for built-in system columns and should not be used when creating custom properties.\n"},"options":{"type":"array","nullable":true,"description":"Active options for `select` and `multiselect` columns. `null` for all other types.\n\n> **Important:** Use the option `key` UUID (not the `name`) when filtering or updating records.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"archivedOptions":{"type":"array","nullable":true,"description":"Retired options for `select` and `multiselect` columns. Archived options are no longer offered when editing a record, but values on records that still reference an archived key keep resolving to its `name`. Move options between `options` and `archivedOptions` to retire or restore them.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object","description":"Type-specific display settings. Usually `{}`."},"ai":{"type":"boolean","description":"Whether this is an AI-filled property (values are generated by AI from a prompt)."},"aiPrompt":{"type":"object","nullable":true,"description":"Prompt document for AI-filled properties. `null` for regular properties."},"aiSettings":{"type":"object","description":"Which data sources the AI may use when filling this property (e.g. `{\"scraping\": true, \"emails\": false, …}`). Only relevant when `ai` is `true`.\n"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Events that re-run the AI property. Only relevant when `ai` is `true`."},"source":{"type":"string","nullable":true,"description":"Origin of the property when it was created by an import or integration (e.g. `hubspot`), otherwise `null`."},"externalId":{"type":"string","nullable":true,"description":"The property's id in the source system when `source` is set."},"importId":{"type":"string","format":"uuid","nullable":true,"description":"The import that created this property, if any."},"aiContextIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"AI context documents attached to an AI-filled property. `[]` by default."},"archived":{"type":"boolean","description":"Soft-deleted flag. Archived columns are excluded from `GET /api/columns` unless you filter for them explicitly (`{\"archived\": true}`).\n"},"archivedAt":{"type":"string","format":"date-time","nullable":true},"archivedById":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}},"ColumnOption":{"type":"object","description":"One selectable option of a `select` / `multiselect` custom property.","required":["key","name"],"properties":{"key":{"type":"string","format":"uuid","description":"The value stored on records for this option. **Generate a UUID yourself** when creating an option and never change it afterwards — it is what `custom.<column-id>` holds on every record that selected this option. Use this (not `name`) when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.\n"},"name":{"type":"string","description":"Human-readable display name for the option. Safe to rename at any time."},"color":{"type":"string","nullable":true,"description":"Optional hex color for UI display (e.g. `#4CAF50`)."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/columns":{"get":{"tags":["Columns"],"summary":"List custom property definitions","description":"Returns the custom property (column) definitions for a workspace. Filter by workspace using the `where` parameter: `{\"workspaceId\": \"<WORKSPACE_UUID>\"}`. Strongly recommended — omitting it returns columns across all workspaces the user has access to.\n\nUse this endpoint to discover the IDs, types, and available options of custom fields before filtering or updating records.\n\nCustom properties are stored in the `custom` object on companies, contacts, and deals. Each custom property is keyed by its UUID (`id` from this response).\n\nFor example, if this endpoint returns a column with `\"id\": \"54e1ca7d-69c3-4b77-8266-8085b5834116\"` and `\"type\": \"select\"`, you can:\n- **Filter** records using: `{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": {\"$in\": [\"<OPTION_KEY_UUID>\", \"<OPTION_KEY_UUID>\"]}}`\n- **Update** a record using: `{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": \"<OPTION_KEY_UUID>\"}`\n\n> **Note:** For `select` and `multiselect` columns, option values are UUIDs stored in the `key` field of each option object. Use the `key` UUID (not the human-readable `name`) when filtering or updating records.\n","operationId":"listColumns","parameters":[{"$ref":"#/components/parameters/fields"},{"$ref":"#/components/parameters/where"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"},{"$ref":"#/components/parameters/orderBy"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Column"}},"total":{"type":"integer","description":"Total number of custom property definitions"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Create a custom property

> Create a new custom property definition on companies, contacts, or deals. The property immediately becomes available for filtering, views, reports, and for writing values into \`custom.\<column-id>\` on records of that entity.\
> \
> Always send \`workspaceId\`, \`entity\`, \`name\`, and \`type\`.\
> \
> \> \*\*Limited server-side validation.\*\* AI type compatibility and formula settings are validated. General column fields and the \`type\` / \`entity\` enums are not otherwise validated. Omitting \`type\` or \`workspaceId\` fails with a \`500 Database insert query failed\`; omitting \`name\` or \`entity\`, or sending an unknown \`type\` / \`entity\` on a non-AI, non-formula property, \*\*succeeds\*\* and silently creates a definition the app cannot render. Values written to \`custom.\<column-id>\` on records are likewise stored as sent, without being checked against the column type — the per-type table on \`Column\` describes what the app, filters, and reports expect, not an enforced contract.\
> \
> \### Select / multiselect options\
> For \`select\` and \`multiselect\` types, pass the initial options in \`options\`. Each option's \`key\` must be a \*\*UUID that you generate\*\* — it becomes the value stored on records, so it must be unique within the column and stable over time. \`name\` is the display label; \`color\` is an optional hex color.\
> \
> \`\`\`json\
> {\
> &#x20; "workspaceId": "dd66c03b-1a84-4c9d-b118-3cc54e21d019",\
> &#x20; "entity": "companies",\
> &#x20; "name": "Industry",\
> &#x20; "type": "select",\
> &#x20; "options": \[\
> &#x20;   { "key": "3e839b5c-b311-4887-b2da-727d2d75cdd6", "name": "SaaS",    "color": "#4CAF50" },\
> &#x20;   { "key": "7f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "name": "Fintech", "color": "#2196F3" }\
> &#x20; ]\
> }\
> \`\`\`\
> \
> \### Using the new property on records\
> The response contains the column \`id\`. Write values with dot-notation on the record — never by replacing the whole \`custom\` object:\
> \
> \`\`\`bash\
> curl -X PATCH "<https://api.zero.inc/api/companies/COMPANY\\_UUID>" \\\
> &#x20; -H "Authorization: Bearer YOUR\_API\_TOKEN" \\\
> &#x20; -H "Content-Type: application/json" \\\
> &#x20; -d '{"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": "3e839b5c-b311-4887-b2da-727d2d75cdd6"}'\
> \`\`\`\
> \
> See the \`type\` field on \`Column\` for the value shape each type expects. The PATCH response echoes only the touched sub-key (\`"custom": {"\<column-id>": value}\`), not the whole \`custom\` object — other keys are preserved; re-\`GET\` the record if you need the merged object. Setting a value to \`null\` keeps the key in \`custom\` with a \`null\` value rather than removing it.\
> \
> \### AI properties\
> Set \`ai: true\` together with \`aiPrompt\` to create an AI-filled property. AI properties are configured most reliably in the app; the API accepts the fields but does not validate the prompt structure.\
> \
> For the custom property types documented here, AI supports \`text\`, \`richtext\`, \`number\`, \`currency\`, \`boolean\`, \`email\`, \`select\`, \`multiselect\`, \`user\`, \`users\`, \`date\`, \`timestamp\`, \`location\`, and \`rating\`. Enabling AI on \`link\`, \`phone\`, \`percentage\`, or company/contact/deal relation types returns \`400\` with an explanation. The same restriction applies when updating an existing property.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Columns","description":"Manage custom property definitions (called *columns* in the API) for companies, contacts, and deals. Use `GET /api/columns` to discover the IDs, types, and options of custom fields before filtering or updating records, and `POST` / `PATCH` / `DELETE` to create, edit, and remove properties.\n\n### Definitions vs. values\nA **column** is the *definition* of a custom property — its name, type, options, and which entity it belongs to. The *values* live on the records themselves, in the `custom` object of each company / contact / deal, keyed by the column's `id`. Creating a column does not touch any records; it just makes `custom.<column-id>` available for writes, filters, views, and reports.\n\n```\nColumn                              Company record\n{ \"id\": \"54e1ca7d-…\",               { \"id\": \"…\",\n  \"entity\": \"companies\",              \"name\": \"Acme\",\n  \"name\": \"Industry\",                 \"custom\": {\n  \"type\": \"select\",                     \"54e1ca7d-…\": \"3e839b5c-…\"   ← option key\n  \"options\": [ {\"key\": \"3e839b5c-…\", \"name\": \"SaaS\"}, … ] }   }\n}\n```\n\n### Creating a property\n`POST /api/columns` with at least `workspaceId`, `entity`, `name`, and `type`. For `select` / `multiselect`, also pass `options` — every option needs a **client-generated UUID** in `key`; that UUID is what gets stored on records, so it must stay stable for the life of the option. Names and colors can be edited freely later.\n\n### Editing a property\n`PATCH /api/columns/{columnId}`. Renaming, changing the description, and editing options are safe. Note that `options` is **replaced wholesale** on write — always send the complete list. Removing an option from `options` does not delete it from records; move it to `archivedOptions` instead so existing values keep resolving to a name. Changing `type` is allowed for a limited set of pairs and triggers a background migration of existing values — see `PATCH` for the supported conversions.\n\n### Deleting a property\n`DELETE /api/columns/{columnId}` permanently removes the definition. Values already stored under that id on records are **not** cleaned up — they simply become invisible/orphaned. Prefer `DELETE …?archive=true` (soft delete) when you may need to restore the property; restore with `PATCH {\"archived\": false}`.\n\n### Reserved / built-in fields\nOnly user-defined properties are columns. Built-in fields (`name`, `domain`, `stage`, `value`, …) are top-level record fields, not columns, and cannot be created or deleted through this endpoint.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"ColumnCreate":{"type":"object","required":["workspaceId","entity","name","type"],"properties":{"workspaceId":{"type":"string","format":"uuid"},"entity":{"type":"string","enum":["companies","contacts","deals"],"description":"Which record type the property is added to. Not validated server-side — other values are stored but unusable. Do not change it later."},"name":{"type":"string","description":"Display name of the property."},"description":{"type":"string","nullable":true,"description":"Optional help text shown in the app."},"type":{"type":"string","enum":["text","richtext","number","percentage","currency","boolean","link","email","phone","select","multiselect","user","users","date","timestamp","location","rating","company","companies","contact","contacts","deal","deals"],"description":"The data type. See the `type` table on `Column` for the value shape each type stores on records."},"options":{"type":"array","description":"Required for `select` / `multiselect` (may be an empty array). Ignored for other types.\n\nGenerate a fresh UUID for each option's `key` — it becomes the value stored on records and must never change afterwards.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object","description":"Type-specific display settings. Omit unless you know you need it."},"ai":{"type":"boolean","default":false,"description":"Set `true` to create an AI-filled property. Requires `aiPrompt` and a supported AI type (see POST /api/columns); unsupported types return 400."},"aiPrompt":{"type":"object","nullable":true,"description":"Prompt document for AI-filled properties. Easiest to author in the app and copy from an existing AI column."},"aiSettings":{"type":"object","description":"Data sources the AI may use, e.g. `{\"scraping\": false, \"emails\": false, \"calendarEvents\": false, \"linkedinMessages\": false, \"files\": false, \"notes\": false}`.\n"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Events that re-run the AI property."}}},"ColumnOption":{"type":"object","description":"One selectable option of a `select` / `multiselect` custom property.","required":["key","name"],"properties":{"key":{"type":"string","format":"uuid","description":"The value stored on records for this option. **Generate a UUID yourself** when creating an option and never change it afterwards — it is what `custom.<column-id>` holds on every record that selected this option. Use this (not `name`) when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.\n"},"name":{"type":"string","description":"Human-readable display name for the option. Safe to rename at any time."},"color":{"type":"string","nullable":true,"description":"Optional hex color for UI display (e.g. `#4CAF50`)."}}},"Column":{"type":"object","description":"A custom property definition. The `id` is used as the key inside the `custom` object on records (`custom.<id>`).","properties":{"id":{"type":"string","format":"uuid","description":"The unique identifier for this custom property. Use this as the key when filtering or updating custom fields on records."},"workspaceId":{"type":"string","format":"uuid"},"entity":{"type":"string","enum":["companies","contacts","deals"],"description":"Which record type this custom property belongs to (plural form). Fixed at creation."},"name":{"type":"string","description":"Human-readable name of the custom property"},"description":{"type":"string","nullable":true,"description":"Optional description of the custom property, shown as help text in the app"},"key":{"type":"string","nullable":true,"description":"Legacy/optional field, usually `null`. Do not rely on it — the path to use in filters and record updates is always `custom.<id>`."},"type":{"type":"string","enum":["text","richtext","number","percentage","currency","boolean","link","email","phone","select","multiselect","user","users","date","timestamp","location","rating","company","companies","contact","contacts","deal","deals"],"description":"The data type of the custom property. This determines the shape of the value stored at `custom.<id>` on records:\n\n| Type | Value stored on records |\n|------|-------------------------|\n| `text` | Plain string |\n| `richtext` | Rich-text string / formatted content |\n| `number` | Number |\n| `percentage` | Number (e.g. `12.5` for 12.5%) |\n| `currency` | Number, displayed in the workspace currency |\n| `boolean` | `true` / `false` |\n| `link` | URL string |\n| `email` | Email address string |\n| `phone` | Phone number string |\n| `select` | One option `key` UUID from `options` |\n| `multiselect` | Array of option `key` UUIDs from `options` |\n| `user` | One workspace user UUID |\n| `users` | Array of workspace user UUIDs |\n| `date` | `YYYY-MM-DD` string |\n| `timestamp` | ISO-8601 date-time string |\n| `location` | Object with any of `city`, `state`, `country`, `continent` (e.g. `{\"city\": \"Berlin\", \"country\": \"Germany\"}`) |\n| `rating` | Integer `1`–`5` |\n| `company` | One company UUID |\n| `companies` | Array of company UUIDs |\n| `contact` | One contact UUID |\n| `contacts` | Array of contact UUIDs |\n| `deal` | One deal UUID |\n| `deals` | Array of deal UUIDs |\n\nOther type values exist internally for built-in system columns and should not be used when creating custom properties.\n"},"options":{"type":"array","nullable":true,"description":"Active options for `select` and `multiselect` columns. `null` for all other types.\n\n> **Important:** Use the option `key` UUID (not the `name`) when filtering or updating records.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"archivedOptions":{"type":"array","nullable":true,"description":"Retired options for `select` and `multiselect` columns. Archived options are no longer offered when editing a record, but values on records that still reference an archived key keep resolving to its `name`. Move options between `options` and `archivedOptions` to retire or restore them.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object","description":"Type-specific display settings. Usually `{}`."},"ai":{"type":"boolean","description":"Whether this is an AI-filled property (values are generated by AI from a prompt)."},"aiPrompt":{"type":"object","nullable":true,"description":"Prompt document for AI-filled properties. `null` for regular properties."},"aiSettings":{"type":"object","description":"Which data sources the AI may use when filling this property (e.g. `{\"scraping\": true, \"emails\": false, …}`). Only relevant when `ai` is `true`.\n"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Events that re-run the AI property. Only relevant when `ai` is `true`."},"source":{"type":"string","nullable":true,"description":"Origin of the property when it was created by an import or integration (e.g. `hubspot`), otherwise `null`."},"externalId":{"type":"string","nullable":true,"description":"The property's id in the source system when `source` is set."},"importId":{"type":"string","format":"uuid","nullable":true,"description":"The import that created this property, if any."},"aiContextIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"AI context documents attached to an AI-filled property. `[]` by default."},"archived":{"type":"boolean","description":"Soft-deleted flag. Archived columns are excluded from `GET /api/columns` unless you filter for them explicitly (`{\"archived\": true}`).\n"},"archivedAt":{"type":"string","format":"date-time","nullable":true},"archivedById":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/columns":{"post":{"tags":["Columns"],"summary":"Create a custom property","description":"Create a new custom property definition on companies, contacts, or deals. The property immediately becomes available for filtering, views, reports, and for writing values into `custom.<column-id>` on records of that entity.\n\nAlways send `workspaceId`, `entity`, `name`, and `type`.\n\n> **Limited server-side validation.** AI type compatibility and formula settings are validated. General column fields and the `type` / `entity` enums are not otherwise validated. Omitting `type` or `workspaceId` fails with a `500 Database insert query failed`; omitting `name` or `entity`, or sending an unknown `type` / `entity` on a non-AI, non-formula property, **succeeds** and silently creates a definition the app cannot render. Values written to `custom.<column-id>` on records are likewise stored as sent, without being checked against the column type — the per-type table on `Column` describes what the app, filters, and reports expect, not an enforced contract.\n\n### Select / multiselect options\nFor `select` and `multiselect` types, pass the initial options in `options`. Each option's `key` must be a **UUID that you generate** — it becomes the value stored on records, so it must be unique within the column and stable over time. `name` is the display label; `color` is an optional hex color.\n\n```json\n{\n  \"workspaceId\": \"dd66c03b-1a84-4c9d-b118-3cc54e21d019\",\n  \"entity\": \"companies\",\n  \"name\": \"Industry\",\n  \"type\": \"select\",\n  \"options\": [\n    { \"key\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\", \"name\": \"SaaS\",    \"color\": \"#4CAF50\" },\n    { \"key\": \"7f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c\", \"name\": \"Fintech\", \"color\": \"#2196F3\" }\n  ]\n}\n```\n\n### Using the new property on records\nThe response contains the column `id`. Write values with dot-notation on the record — never by replacing the whole `custom` object:\n\n```bash\ncurl -X PATCH \"https://api.zero.inc/api/companies/COMPANY_UUID\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"custom.54e1ca7d-69c3-4b77-8266-8085b5834116\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\"}'\n```\n\nSee the `type` field on `Column` for the value shape each type expects. The PATCH response echoes only the touched sub-key (`\"custom\": {\"<column-id>\": value}`), not the whole `custom` object — other keys are preserved; re-`GET` the record if you need the merged object. Setting a value to `null` keeps the key in `custom` with a `null` value rather than removing it.\n\n### AI properties\nSet `ai: true` together with `aiPrompt` to create an AI-filled property. AI properties are configured most reliably in the app; the API accepts the fields but does not validate the prompt structure.\n\nFor the custom property types documented here, AI supports `text`, `richtext`, `number`, `currency`, `boolean`, `email`, `select`, `multiselect`, `user`, `users`, `date`, `timestamp`, `location`, and `rating`. Enabling AI on `link`, `phone`, `percentage`, or company/contact/deal relation types returns `400` with an explanation. The same restriction applies when updating an existing property.\n","operationId":"createColumn","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ColumnCreate"}}}},"responses":{"200":{"description":"Custom property created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Column"},"sideEffects":{"type":"array","items":{}}}}}}},"400":{"description":"Unsupported AI property type or invalid formula settings."},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Get a custom property

> Returns a single custom property definition by ID.\
> \
> An unknown or deleted ID returns \`200\` with an empty \`data\` object (\`{}\`), not a \`404\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Columns","description":"Manage custom property definitions (called *columns* in the API) for companies, contacts, and deals. Use `GET /api/columns` to discover the IDs, types, and options of custom fields before filtering or updating records, and `POST` / `PATCH` / `DELETE` to create, edit, and remove properties.\n\n### Definitions vs. values\nA **column** is the *definition* of a custom property — its name, type, options, and which entity it belongs to. The *values* live on the records themselves, in the `custom` object of each company / contact / deal, keyed by the column's `id`. Creating a column does not touch any records; it just makes `custom.<column-id>` available for writes, filters, views, and reports.\n\n```\nColumn                              Company record\n{ \"id\": \"54e1ca7d-…\",               { \"id\": \"…\",\n  \"entity\": \"companies\",              \"name\": \"Acme\",\n  \"name\": \"Industry\",                 \"custom\": {\n  \"type\": \"select\",                     \"54e1ca7d-…\": \"3e839b5c-…\"   ← option key\n  \"options\": [ {\"key\": \"3e839b5c-…\", \"name\": \"SaaS\"}, … ] }   }\n}\n```\n\n### Creating a property\n`POST /api/columns` with at least `workspaceId`, `entity`, `name`, and `type`. For `select` / `multiselect`, also pass `options` — every option needs a **client-generated UUID** in `key`; that UUID is what gets stored on records, so it must stay stable for the life of the option. Names and colors can be edited freely later.\n\n### Editing a property\n`PATCH /api/columns/{columnId}`. Renaming, changing the description, and editing options are safe. Note that `options` is **replaced wholesale** on write — always send the complete list. Removing an option from `options` does not delete it from records; move it to `archivedOptions` instead so existing values keep resolving to a name. Changing `type` is allowed for a limited set of pairs and triggers a background migration of existing values — see `PATCH` for the supported conversions.\n\n### Deleting a property\n`DELETE /api/columns/{columnId}` permanently removes the definition. Values already stored under that id on records are **not** cleaned up — they simply become invisible/orphaned. Prefer `DELETE …?archive=true` (soft delete) when you may need to restore the property; restore with `PATCH {\"archived\": false}`.\n\n### Reserved / built-in fields\nOnly user-defined properties are columns. Built-in fields (`name`, `domain`, `stage`, `value`, …) are top-level record fields, not columns, and cannot be created or deleted through this endpoint.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"parameters":{"fields":{"name":"fields","in":"query","description":"Comma-separated list of fields to return. Defaults to all fields.\n\nSupports **relation expansion** with dot-notation: a path like `company.name` embeds the named field of the related record, and `quotes.id` embeds the `id` of each related quote. Use `.*` to embed full related objects, e.g. `quotes.*`. Related records are returned nested on the response — as an object for to-one relations (e.g. `company`) or an array for to-many relations (e.g. `quotes`). Archived related records are excluded.\n\nExample: `GET /api/deals/{dealId}?fields=id,name,quotes.id` returns the deal with a `quotes` array of `{\"id\": \"<QUOTE_UUID>\"}` entries.\n","schema":{"type":"string"}}},"schemas":{"Column":{"type":"object","description":"A custom property definition. The `id` is used as the key inside the `custom` object on records (`custom.<id>`).","properties":{"id":{"type":"string","format":"uuid","description":"The unique identifier for this custom property. Use this as the key when filtering or updating custom fields on records."},"workspaceId":{"type":"string","format":"uuid"},"entity":{"type":"string","enum":["companies","contacts","deals"],"description":"Which record type this custom property belongs to (plural form). Fixed at creation."},"name":{"type":"string","description":"Human-readable name of the custom property"},"description":{"type":"string","nullable":true,"description":"Optional description of the custom property, shown as help text in the app"},"key":{"type":"string","nullable":true,"description":"Legacy/optional field, usually `null`. Do not rely on it — the path to use in filters and record updates is always `custom.<id>`."},"type":{"type":"string","enum":["text","richtext","number","percentage","currency","boolean","link","email","phone","select","multiselect","user","users","date","timestamp","location","rating","company","companies","contact","contacts","deal","deals"],"description":"The data type of the custom property. This determines the shape of the value stored at `custom.<id>` on records:\n\n| Type | Value stored on records |\n|------|-------------------------|\n| `text` | Plain string |\n| `richtext` | Rich-text string / formatted content |\n| `number` | Number |\n| `percentage` | Number (e.g. `12.5` for 12.5%) |\n| `currency` | Number, displayed in the workspace currency |\n| `boolean` | `true` / `false` |\n| `link` | URL string |\n| `email` | Email address string |\n| `phone` | Phone number string |\n| `select` | One option `key` UUID from `options` |\n| `multiselect` | Array of option `key` UUIDs from `options` |\n| `user` | One workspace user UUID |\n| `users` | Array of workspace user UUIDs |\n| `date` | `YYYY-MM-DD` string |\n| `timestamp` | ISO-8601 date-time string |\n| `location` | Object with any of `city`, `state`, `country`, `continent` (e.g. `{\"city\": \"Berlin\", \"country\": \"Germany\"}`) |\n| `rating` | Integer `1`–`5` |\n| `company` | One company UUID |\n| `companies` | Array of company UUIDs |\n| `contact` | One contact UUID |\n| `contacts` | Array of contact UUIDs |\n| `deal` | One deal UUID |\n| `deals` | Array of deal UUIDs |\n\nOther type values exist internally for built-in system columns and should not be used when creating custom properties.\n"},"options":{"type":"array","nullable":true,"description":"Active options for `select` and `multiselect` columns. `null` for all other types.\n\n> **Important:** Use the option `key` UUID (not the `name`) when filtering or updating records.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"archivedOptions":{"type":"array","nullable":true,"description":"Retired options for `select` and `multiselect` columns. Archived options are no longer offered when editing a record, but values on records that still reference an archived key keep resolving to its `name`. Move options between `options` and `archivedOptions` to retire or restore them.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object","description":"Type-specific display settings. Usually `{}`."},"ai":{"type":"boolean","description":"Whether this is an AI-filled property (values are generated by AI from a prompt)."},"aiPrompt":{"type":"object","nullable":true,"description":"Prompt document for AI-filled properties. `null` for regular properties."},"aiSettings":{"type":"object","description":"Which data sources the AI may use when filling this property (e.g. `{\"scraping\": true, \"emails\": false, …}`). Only relevant when `ai` is `true`.\n"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Events that re-run the AI property. Only relevant when `ai` is `true`."},"source":{"type":"string","nullable":true,"description":"Origin of the property when it was created by an import or integration (e.g. `hubspot`), otherwise `null`."},"externalId":{"type":"string","nullable":true,"description":"The property's id in the source system when `source` is set."},"importId":{"type":"string","format":"uuid","nullable":true,"description":"The import that created this property, if any."},"aiContextIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"AI context documents attached to an AI-filled property. `[]` by default."},"archived":{"type":"boolean","description":"Soft-deleted flag. Archived columns are excluded from `GET /api/columns` unless you filter for them explicitly (`{\"archived\": true}`).\n"},"archivedAt":{"type":"string","format":"date-time","nullable":true},"archivedById":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}},"ColumnOption":{"type":"object","description":"One selectable option of a `select` / `multiselect` custom property.","required":["key","name"],"properties":{"key":{"type":"string","format":"uuid","description":"The value stored on records for this option. **Generate a UUID yourself** when creating an option and never change it afterwards — it is what `custom.<column-id>` holds on every record that selected this option. Use this (not `name`) when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.\n"},"name":{"type":"string","description":"Human-readable display name for the option. Safe to rename at any time."},"color":{"type":"string","nullable":true,"description":"Optional hex color for UI display (e.g. `#4CAF50`)."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/columns/{columnId}":{"get":{"tags":["Columns"],"summary":"Get a custom property","description":"Returns a single custom property definition by ID.\n\nAn unknown or deleted ID returns `200` with an empty `data` object (`{}`), not a `404`.\n","operationId":"getColumn","parameters":[{"name":"columnId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/fields"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Column"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Delete a custom property

> Remove a custom property definition.\
> \
> \### Permanent delete (default)\
> \`DELETE /api/columns/{columnId}\` permanently deletes the definition. Returns \`{"data": 1}\` on success and \`{"data": 0}\` when no matching non-archived column exists. There is no undo.\
> \
> \### Soft delete (archive)\
> \`DELETE /api/columns/{columnId}?archive=true\` sets \`archived: true\` instead. The response is \`{"data": \[\<archived column>]}\` — an array containing the full row with \`archived: true\`, \`archivedAt\`, and \`archivedById\` set (or \`\[]\` if nothing was archived). Archived columns are hidden from the app and excluded from default \`GET /api/columns\` results; \`GET /api/columns/{columnId}\` returns \`200 {}\` for them. List them with \`{"archived": true}\` in \`where\`, and restore with \`PATCH /api/columns/{columnId} {"archived": false}\`. An archived column cannot be hard-deleted directly — \`DELETE\` without \`?archive=true\` returns \`{"data": 0}\` — restore it first, then delete.\
> \
> \> \*\*Warning:\*\* Deleting a property does not remove its values from records. Any \`custom.\<column-id>\` values already stored on companies, contacts, or deals stay in the \`custom\` object as orphaned keys — they are no longer shown in the app, filters, or reports. Views that reference the column in \`settings.columns\` or \`filters\` keep the stale \`custom.\<column-id>\` reference in their stored JSON; the app simply can no longer resolve it. The \`id\` cannot be re-created, so a new property with the same name will not pick the old values back up.\
> \
> A non-UUID \`columnId\` returns \`500\`, not \`404\`.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Columns","description":"Manage custom property definitions (called *columns* in the API) for companies, contacts, and deals. Use `GET /api/columns` to discover the IDs, types, and options of custom fields before filtering or updating records, and `POST` / `PATCH` / `DELETE` to create, edit, and remove properties.\n\n### Definitions vs. values\nA **column** is the *definition* of a custom property — its name, type, options, and which entity it belongs to. The *values* live on the records themselves, in the `custom` object of each company / contact / deal, keyed by the column's `id`. Creating a column does not touch any records; it just makes `custom.<column-id>` available for writes, filters, views, and reports.\n\n```\nColumn                              Company record\n{ \"id\": \"54e1ca7d-…\",               { \"id\": \"…\",\n  \"entity\": \"companies\",              \"name\": \"Acme\",\n  \"name\": \"Industry\",                 \"custom\": {\n  \"type\": \"select\",                     \"54e1ca7d-…\": \"3e839b5c-…\"   ← option key\n  \"options\": [ {\"key\": \"3e839b5c-…\", \"name\": \"SaaS\"}, … ] }   }\n}\n```\n\n### Creating a property\n`POST /api/columns` with at least `workspaceId`, `entity`, `name`, and `type`. For `select` / `multiselect`, also pass `options` — every option needs a **client-generated UUID** in `key`; that UUID is what gets stored on records, so it must stay stable for the life of the option. Names and colors can be edited freely later.\n\n### Editing a property\n`PATCH /api/columns/{columnId}`. Renaming, changing the description, and editing options are safe. Note that `options` is **replaced wholesale** on write — always send the complete list. Removing an option from `options` does not delete it from records; move it to `archivedOptions` instead so existing values keep resolving to a name. Changing `type` is allowed for a limited set of pairs and triggers a background migration of existing values — see `PATCH` for the supported conversions.\n\n### Deleting a property\n`DELETE /api/columns/{columnId}` permanently removes the definition. Values already stored under that id on records are **not** cleaned up — they simply become invisible/orphaned. Prefer `DELETE …?archive=true` (soft delete) when you may need to restore the property; restore with `PATCH {\"archived\": false}`.\n\n### Reserved / built-in fields\nOnly user-defined properties are columns. Built-in fields (`name`, `domain`, `stage`, `value`, …) are top-level record fields, not columns, and cannot be created or deleted through this endpoint.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"Column":{"type":"object","description":"A custom property definition. The `id` is used as the key inside the `custom` object on records (`custom.<id>`).","properties":{"id":{"type":"string","format":"uuid","description":"The unique identifier for this custom property. Use this as the key when filtering or updating custom fields on records."},"workspaceId":{"type":"string","format":"uuid"},"entity":{"type":"string","enum":["companies","contacts","deals"],"description":"Which record type this custom property belongs to (plural form). Fixed at creation."},"name":{"type":"string","description":"Human-readable name of the custom property"},"description":{"type":"string","nullable":true,"description":"Optional description of the custom property, shown as help text in the app"},"key":{"type":"string","nullable":true,"description":"Legacy/optional field, usually `null`. Do not rely on it — the path to use in filters and record updates is always `custom.<id>`."},"type":{"type":"string","enum":["text","richtext","number","percentage","currency","boolean","link","email","phone","select","multiselect","user","users","date","timestamp","location","rating","company","companies","contact","contacts","deal","deals"],"description":"The data type of the custom property. This determines the shape of the value stored at `custom.<id>` on records:\n\n| Type | Value stored on records |\n|------|-------------------------|\n| `text` | Plain string |\n| `richtext` | Rich-text string / formatted content |\n| `number` | Number |\n| `percentage` | Number (e.g. `12.5` for 12.5%) |\n| `currency` | Number, displayed in the workspace currency |\n| `boolean` | `true` / `false` |\n| `link` | URL string |\n| `email` | Email address string |\n| `phone` | Phone number string |\n| `select` | One option `key` UUID from `options` |\n| `multiselect` | Array of option `key` UUIDs from `options` |\n| `user` | One workspace user UUID |\n| `users` | Array of workspace user UUIDs |\n| `date` | `YYYY-MM-DD` string |\n| `timestamp` | ISO-8601 date-time string |\n| `location` | Object with any of `city`, `state`, `country`, `continent` (e.g. `{\"city\": \"Berlin\", \"country\": \"Germany\"}`) |\n| `rating` | Integer `1`–`5` |\n| `company` | One company UUID |\n| `companies` | Array of company UUIDs |\n| `contact` | One contact UUID |\n| `contacts` | Array of contact UUIDs |\n| `deal` | One deal UUID |\n| `deals` | Array of deal UUIDs |\n\nOther type values exist internally for built-in system columns and should not be used when creating custom properties.\n"},"options":{"type":"array","nullable":true,"description":"Active options for `select` and `multiselect` columns. `null` for all other types.\n\n> **Important:** Use the option `key` UUID (not the `name`) when filtering or updating records.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"archivedOptions":{"type":"array","nullable":true,"description":"Retired options for `select` and `multiselect` columns. Archived options are no longer offered when editing a record, but values on records that still reference an archived key keep resolving to its `name`. Move options between `options` and `archivedOptions` to retire or restore them.\n","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object","description":"Type-specific display settings. Usually `{}`."},"ai":{"type":"boolean","description":"Whether this is an AI-filled property (values are generated by AI from a prompt)."},"aiPrompt":{"type":"object","nullable":true,"description":"Prompt document for AI-filled properties. `null` for regular properties."},"aiSettings":{"type":"object","description":"Which data sources the AI may use when filling this property (e.g. `{\"scraping\": true, \"emails\": false, …}`). Only relevant when `ai` is `true`.\n"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Events that re-run the AI property. Only relevant when `ai` is `true`."},"source":{"type":"string","nullable":true,"description":"Origin of the property when it was created by an import or integration (e.g. `hubspot`), otherwise `null`."},"externalId":{"type":"string","nullable":true,"description":"The property's id in the source system when `source` is set."},"importId":{"type":"string","format":"uuid","nullable":true,"description":"The import that created this property, if any."},"aiContextIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"AI context documents attached to an AI-filled property. `[]` by default."},"archived":{"type":"boolean","description":"Soft-deleted flag. Archived columns are excluded from `GET /api/columns` unless you filter for them explicitly (`{\"archived\": true}`).\n"},"archivedAt":{"type":"string","format":"date-time","nullable":true},"archivedById":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdById":{"type":"string","format":"uuid","nullable":true},"updatedById":{"type":"string","format":"uuid","nullable":true}}},"ColumnOption":{"type":"object","description":"One selectable option of a `select` / `multiselect` custom property.","required":["key","name"],"properties":{"key":{"type":"string","format":"uuid","description":"The value stored on records for this option. **Generate a UUID yourself** when creating an option and never change it afterwards — it is what `custom.<column-id>` holds on every record that selected this option. Use this (not `name`) when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.\n"},"name":{"type":"string","description":"Human-readable display name for the option. Safe to rename at any time."},"color":{"type":"string","nullable":true,"description":"Optional hex color for UI display (e.g. `#4CAF50`)."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/columns/{columnId}":{"delete":{"tags":["Columns"],"summary":"Delete a custom property","description":"Remove a custom property definition.\n\n### Permanent delete (default)\n`DELETE /api/columns/{columnId}` permanently deletes the definition. Returns `{\"data\": 1}` on success and `{\"data\": 0}` when no matching non-archived column exists. There is no undo.\n\n### Soft delete (archive)\n`DELETE /api/columns/{columnId}?archive=true` sets `archived: true` instead. The response is `{\"data\": [<archived column>]}` — an array containing the full row with `archived: true`, `archivedAt`, and `archivedById` set (or `[]` if nothing was archived). Archived columns are hidden from the app and excluded from default `GET /api/columns` results; `GET /api/columns/{columnId}` returns `200 {}` for them. List them with `{\"archived\": true}` in `where`, and restore with `PATCH /api/columns/{columnId} {\"archived\": false}`. An archived column cannot be hard-deleted directly — `DELETE` without `?archive=true` returns `{\"data\": 0}` — restore it first, then delete.\n\n> **Warning:** Deleting a property does not remove its values from records. Any `custom.<column-id>` values already stored on companies, contacts, or deals stay in the `custom` object as orphaned keys — they are no longer shown in the app, filters, or reports. Views that reference the column in `settings.columns` or `filters` keep the stale `custom.<column-id>` reference in their stored JSON; the app simply can no longer resolve it. The `id` cannot be re-created, so a new property with the same name will not pick the old values back up.\n\nA non-UUID `columnId` returns `500`, not `404`.\n","operationId":"deleteColumn","parameters":[{"name":"columnId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"archive","in":"query","required":false,"description":"Set to `true` to soft-delete (archive) instead of permanently deleting. Omit for a permanent delete.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Deletion result. `data` is an integer for a permanent delete and an array of archived rows when `?archive=true` was used.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"oneOf":[{"type":"integer","description":"Permanent delete: `1` if deleted, `0` if not found"},{"type":"array","description":"Archive: the archived column row(s), `[]` if nothing was archived","items":{"$ref":"#/components/schemas/Column"}}]},"sideEffects":{"type":"array","items":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````

## Update a custom property

> Update a custom property definition. Only the fields you send are changed. The response \`data\` echoes the fields you sent (even if unchanged) plus \`id\`, \`updatedAt\`, and \`updatedById\`. Unknown fields are silently ignored; an unknown \`columnId\` returns \`200\` with no \`data\` rather than a \`404\`.\
> \
> \### Renaming / description\
> Send \`name\` and/or \`description\`. Values on records are unaffected — they are keyed by the column \`id\`, not the name.\
> \
> \### Editing options (select / multiselect)\
> \`options\` is \*\*replaced as a whole\*\* on every write. To add, rename, recolor, or reorder options, send the full desired list. Keep the existing \`key\` UUIDs for options that already exist — changing a key orphans every record value that used the old one.\
> \
> To retire an option without breaking records that still hold it, remove it from \`options\` and add it to \`archivedOptions\`. Archived options are no longer offered when editing records, but existing values keep resolving to their name. To restore, move it back. Both arrays are replaced wholesale, so send the complete list for each. Option objects are stored as sent — the API does not check that each has a \`key\`, so make sure you include one.\
> \
> \`\`\`json\
> {\
> &#x20; "options": \[\
> &#x20;   { "key": "3e839b5c-b311-4887-b2da-727d2d75cdd6", "name": "SaaS",  "color": "#4CAF50" },\
> &#x20;   { "key": "9c8b7a6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d", "name": "Health", "color": "#E91E63" }\
> &#x20; ],\
> &#x20; "archivedOptions": \[\
> &#x20;   { "key": "7f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "name": "Fintech", "color": "#2196F3" }\
> &#x20; ]\
> }\
> \`\`\`\
> \
> \### Changing the type\
> Changing \`type\` between the pairs below kicks off a \*\*background migration\*\* of the values already stored on records. The PATCH returns immediately (before the migration runs; \`sideEffects\` stays \`\[]\`), so re-read records afterwards rather than expecting the PATCH response to reflect it. Locally the migration finishes within milliseconds; on large workspaces allow a little longer. Only non-archived records are migrated.\
> \
> \| From → To | What happens to existing values |\
> \|-----------|----------------------------------|\
> \| \`text\` / \`number\` / \`percentage\` / \`boolean\` / \`link\` / \`currency\` / \`email\` / \`phone\` → \`select\` or \`multiselect\` | Each distinct plain value becomes an option (matched by name against existing active \*and\* archived options; new options are appended to \`options\` with a generated \`key\` and no \`color\`); record values are rewritten to the option \`key\`. |\
> \| \`select\` / \`multiselect\` → any plain type above | Option keys are resolved to the option's \`name\` (active or archived). Target \`text\` / \`link\` / \`email\` / \`phone\` gets the name; \`multiselect\` → \`text\` joins multiple names with \`", "\`. Target \`number\` / \`currency\` / \`percentage\` keeps only numeric names, otherwise \`null\`. Target \`boolean\` maps \`true\`/\`false\`/\`yes\`/\`no\`, otherwise \`null\`. For \`multiselect\` → numeric/boolean only the first value is considered. The \`options\` array stays on the definition (it is not cleared). |\
> \| \`select\` → \`multiselect\`, \`user\` → \`users\`, \`company\` → \`companies\`, \`contact\` → \`contacts\`, \`deal\` → \`deals\`, \`task\` → \`tasks\` | Single values are wrapped in an array (whatever the value is, active or archived option). |\
> \| The reverse of each pair above | Arrays are collapsed to their first element. |\
> \
> Any other \`type\` change (e.g. \`text\` → \`number\`, \`select\` → \`date\`) is accepted on the definition but does \*\*not\*\* migrate values — records keep whatever was stored, which will usually render as empty or invalid under the new type. For non-AI properties, \`type\` is not validated, so a typo is stored as-is. Avoid changes outside the listed pairs.\
> \
> \### AI compatibility\
> The supported AI types listed on \`POST /api/columns\` also apply to updates. A resulting configuration with \`ai: true\` and an unsupported type returns \`400\`. To change an AI property to an unsupported type, send \`ai: false\` in the same request. Disabling AI preserves \`aiPrompt\`, \`aiSettings\`, and existing record values.\
> \
> \### Fields you should not change\
> \`workspaceId\` and \`entity\` are set at creation and must not be changed — values stored under this column id live on the original entity's records and would be orphaned. To move a property between entities, create a new property on the target entity instead.<br>

````json
{"openapi":"3.0.3","info":{"title":"Zero API","version":"1.14.0"},"tags":[{"name":"Columns","description":"Manage custom property definitions (called *columns* in the API) for companies, contacts, and deals. Use `GET /api/columns` to discover the IDs, types, and options of custom fields before filtering or updating records, and `POST` / `PATCH` / `DELETE` to create, edit, and remove properties.\n\n### Definitions vs. values\nA **column** is the *definition* of a custom property — its name, type, options, and which entity it belongs to. The *values* live on the records themselves, in the `custom` object of each company / contact / deal, keyed by the column's `id`. Creating a column does not touch any records; it just makes `custom.<column-id>` available for writes, filters, views, and reports.\n\n```\nColumn                              Company record\n{ \"id\": \"54e1ca7d-…\",               { \"id\": \"…\",\n  \"entity\": \"companies\",              \"name\": \"Acme\",\n  \"name\": \"Industry\",                 \"custom\": {\n  \"type\": \"select\",                     \"54e1ca7d-…\": \"3e839b5c-…\"   ← option key\n  \"options\": [ {\"key\": \"3e839b5c-…\", \"name\": \"SaaS\"}, … ] }   }\n}\n```\n\n### Creating a property\n`POST /api/columns` with at least `workspaceId`, `entity`, `name`, and `type`. For `select` / `multiselect`, also pass `options` — every option needs a **client-generated UUID** in `key`; that UUID is what gets stored on records, so it must stay stable for the life of the option. Names and colors can be edited freely later.\n\n### Editing a property\n`PATCH /api/columns/{columnId}`. Renaming, changing the description, and editing options are safe. Note that `options` is **replaced wholesale** on write — always send the complete list. Removing an option from `options` does not delete it from records; move it to `archivedOptions` instead so existing values keep resolving to a name. Changing `type` is allowed for a limited set of pairs and triggers a background migration of existing values — see `PATCH` for the supported conversions.\n\n### Deleting a property\n`DELETE /api/columns/{columnId}` permanently removes the definition. Values already stored under that id on records are **not** cleaned up — they simply become invisible/orphaned. Prefer `DELETE …?archive=true` (soft delete) when you may need to restore the property; restore with `PATCH {\"archived\": false}`.\n\n### Reserved / built-in fields\nOnly user-defined properties are columns. Built-in fields (`name`, `domain`, `stage`, `value`, …) are top-level record fields, not columns, and cannot be created or deleted through this endpoint.\n"}],"servers":[{"url":"https://api.zero.inc","description":"Production server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)"}},"schemas":{"ColumnUpdate":{"type":"object","description":"Fields that can be patched on a custom property. Send only what you want to change. `options` and `archivedOptions` are replaced wholesale — always send the complete list.\n","properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true},"type":{"type":"string","description":"New data type. Only the conversions listed on `PATCH /api/columns/{columnId}` migrate existing record values (plain ↔ select/multiselect, single ↔ multi). Other changes leave record values untouched and usually unusable.\n"},"options":{"type":"array","description":"Full replacement list of active options. Keep existing `key` values stable; add new options with freshly generated UUIDs.","items":{"$ref":"#/components/schemas/ColumnOption"}},"archivedOptions":{"type":"array","nullable":true,"description":"Full replacement list of retired options. Move an option here (and out of `options`) to hide it from pickers without breaking records that already hold it. Send `null` or `[]` to clear.","items":{"$ref":"#/components/schemas/ColumnOption"}},"settings":{"type":"object"},"ai":{"type":"boolean","description":"The resulting type must support AI when true. Send false together with a change to an unsupported type; otherwise the update returns 400."},"aiPrompt":{"type":"object","nullable":true},"aiSettings":{"type":"object"},"aiTriggers":{"type":"array","nullable":true,"items":{"type":"string"}},"archived":{"type":"boolean","description":"Set `false` to restore a property that was soft-deleted with `DELETE …?archive=true`."}}},"ColumnOption":{"type":"object","description":"One selectable option of a `select` / `multiselect` custom property.","required":["key","name"],"properties":{"key":{"type":"string","format":"uuid","description":"The value stored on records for this option. **Generate a UUID yourself** when creating an option and never change it afterwards — it is what `custom.<column-id>` holds on every record that selected this option. Use this (not `name`) when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.\n"},"name":{"type":"string","description":"Human-readable display name for the option. Safe to rename at any time."},"color":{"type":"string","nullable":true,"description":"Optional hex color for UI display (e.g. `#4CAF50`)."}}}},"responses":{"Unauthorized":{"description":"Authentication failed or token is invalid/expired","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"paths":{"/api/columns/{columnId}":{"patch":{"tags":["Columns"],"summary":"Update a custom property","description":"Update a custom property definition. Only the fields you send are changed. The response `data` echoes the fields you sent (even if unchanged) plus `id`, `updatedAt`, and `updatedById`. Unknown fields are silently ignored; an unknown `columnId` returns `200` with no `data` rather than a `404`.\n\n### Renaming / description\nSend `name` and/or `description`. Values on records are unaffected — they are keyed by the column `id`, not the name.\n\n### Editing options (select / multiselect)\n`options` is **replaced as a whole** on every write. To add, rename, recolor, or reorder options, send the full desired list. Keep the existing `key` UUIDs for options that already exist — changing a key orphans every record value that used the old one.\n\nTo retire an option without breaking records that still hold it, remove it from `options` and add it to `archivedOptions`. Archived options are no longer offered when editing records, but existing values keep resolving to their name. To restore, move it back. Both arrays are replaced wholesale, so send the complete list for each. Option objects are stored as sent — the API does not check that each has a `key`, so make sure you include one.\n\n```json\n{\n  \"options\": [\n    { \"key\": \"3e839b5c-b311-4887-b2da-727d2d75cdd6\", \"name\": \"SaaS\",  \"color\": \"#4CAF50\" },\n    { \"key\": \"9c8b7a6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d\", \"name\": \"Health\", \"color\": \"#E91E63\" }\n  ],\n  \"archivedOptions\": [\n    { \"key\": \"7f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c\", \"name\": \"Fintech\", \"color\": \"#2196F3\" }\n  ]\n}\n```\n\n### Changing the type\nChanging `type` between the pairs below kicks off a **background migration** of the values already stored on records. The PATCH returns immediately (before the migration runs; `sideEffects` stays `[]`), so re-read records afterwards rather than expecting the PATCH response to reflect it. Locally the migration finishes within milliseconds; on large workspaces allow a little longer. Only non-archived records are migrated.\n\n| From → To | What happens to existing values |\n|-----------|----------------------------------|\n| `text` / `number` / `percentage` / `boolean` / `link` / `currency` / `email` / `phone` → `select` or `multiselect` | Each distinct plain value becomes an option (matched by name against existing active *and* archived options; new options are appended to `options` with a generated `key` and no `color`); record values are rewritten to the option `key`. |\n| `select` / `multiselect` → any plain type above | Option keys are resolved to the option's `name` (active or archived). Target `text` / `link` / `email` / `phone` gets the name; `multiselect` → `text` joins multiple names with `\", \"`. Target `number` / `currency` / `percentage` keeps only numeric names, otherwise `null`. Target `boolean` maps `true`/`false`/`yes`/`no`, otherwise `null`. For `multiselect` → numeric/boolean only the first value is considered. The `options` array stays on the definition (it is not cleared). |\n| `select` → `multiselect`, `user` → `users`, `company` → `companies`, `contact` → `contacts`, `deal` → `deals`, `task` → `tasks` | Single values are wrapped in an array (whatever the value is, active or archived option). |\n| The reverse of each pair above | Arrays are collapsed to their first element. |\n\nAny other `type` change (e.g. `text` → `number`, `select` → `date`) is accepted on the definition but does **not** migrate values — records keep whatever was stored, which will usually render as empty or invalid under the new type. For non-AI properties, `type` is not validated, so a typo is stored as-is. Avoid changes outside the listed pairs.\n\n### AI compatibility\nThe supported AI types listed on `POST /api/columns` also apply to updates. A resulting configuration with `ai: true` and an unsupported type returns `400`. To change an AI property to an unsupported type, send `ai: false` in the same request. Disabling AI preserves `aiPrompt`, `aiSettings`, and existing record values.\n\n### Fields you should not change\n`workspaceId` and `entity` are set at creation and must not be changed — values stored under this column id live on the original entity's records and would be orphaned. To move a property between entities, create a new property on the target entity instead.\n","operationId":"updateColumn","parameters":[{"name":"columnId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ColumnUpdate"}}}},"responses":{"200":{"description":"Property updated. `data` contains the fields you sent plus `id`, `updatedAt`, and `updatedById`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"sideEffects":{"type":"array","items":{}}}}}}},"400":{"description":"Unsupported AI property type or invalid formula settings."},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}
````


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.zero.inc/developers/api/columns.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
