Skip to content

Databases & fields

A database is a user-defined type — Tasks, Clients, Articles. It owns fields, records, and views, and belongs to exactly one space. Every database has one built-in title field (required, undeletable) plus system fields. Creating or changing a database is a runtime API call, not a migration — schema is data.

Each field has a stable api_name (used by the API and MCP), a display name, and type-specific config.

Type Notes
title Built-in, one per database; promoted to a real column for fast search and pickers.
text Single- or multi-line ({multiline}).
number Precision + format (plain / percent / currency).
select / multi_select Options are first-class rows with stable IDs (see below).
workflow A single-select-like canonical status — see below. At most one per database.
date Optionally includes a time ({include_time}).
checkbox Boolean.
user A person; single or multi.
url, email Validated text.
relation A link to another database — see relations.
lookup, rollup Derived from a relation — see lookups & rollups.
formula Computed from other fields — see formulas.
button Runs actions on click — see automations & buttons.
created_at, updated_at, created_by System, read-only. Filterable and sortable like any other field, with the operators each type supports.

created_at and updated_at get their own ⋯ on hover, same as every other column — but a reduced one: Filter by this field, Sort by this field, Hide field. No Edit field, Change type, or Delete field, because a system field is read-only and its position is fixed; there is nothing those three would do. There’s also no drag handle — a system column’s position can’t be reordered, matching what the server has always enforced.

A record opens as one main column and a fixed side panel.

  • Main column, top to bottom: the title, a status strip (workflow status, assignee, due date and who’s watching), the fields in labelled groups — Details, Links, Computed and System — then the Description, any embedded collections (sub-items and the like), and Attachments.
  • Attachments appear only when they have something to show — the database has an attachment field, or this record already has files. Existing files are never hidden.
  • Empty collections hide by default. A collection on a record (many-to-many, or the many side of a one-to-many) is hidden while it has nothing in it; single references and ordinary fields keep showing. A field’s own menu overrides this either way: Hide when empty or Always show (even empty), and the menu shows the setting actually in force.
  • An embedded collection’s filter is yours. Filtering, sorting, colouring or choosing columns on a collection like Sub-items is a personal setting: it applies to you only, in every record of that database, and a Personal · Reset badge appears beside the collection while it’s active. Reset drops the whole override and returns you to the shared default the field is configured with. Nobody else’s view of the collection changes.
  • Comments show where they came from. Each comment carries the same source badge the activity feed uses, so a comment an agent posted doesn’t read as a person’s. A comment from before sources were recorded shows the “source not recorded” badge rather than reading as human.
  • The side panel (about 344px, hidden on a narrow window) has three tabs: Activity, Comments and About. About holds the Watchers list, Mentioned in, and a Details block — the database, space, record number, created and updated times, and your access.
  • Watching is a button now. A Watch toggle in the record header (and in the About tab) follows the record: you’re notified when it changes. See mentions and notifications.
  • One list of fields, not zones. The old split into a top strip, a sidebar and a body no longer shows; fields are one grouped area (the stored arrangement is kept, just not shown).
  • The app calls an item by its database’s own noun — a row in Tasks is a task — and falls back to “item”. These docs say “record” for all of them.

A record shows a row of field chips directly under its title — the most-read space on the record screen. You choose which fields go there from the record itself, with Top row, which sits with the other field controls (“New field”, “N hidden”) rather than in a separate place.

  • It’s per database. Pick a field once and every record in that database shows it in that row — not just the record you were on, and not just for you. An Article might pin its publish date; a Lead, its value.
  • Add or remove any field from the Top row menu. Clearing every field returns the row to its automatic default.
  • A database nobody has configured looks exactly as it did before — nothing is reshuffled when this ships, and a database only changes when someone picks a field.

A text or number field can be marked unique — a second record can’t save with a value that duplicates an existing one in that field. Turning it on scans existing rows first and refuses with the conflicts named, rather than silently letting existing duplicates through unnoticed. Multiple empty values are always allowed, regardless of the setting — uniqueness only compares values that are actually present. By default, comparison also folds case and trims whitespace first, so "SKU-1" and " sku-1 " count as the same value; this normalization can be turned off.

Enforced everywhere a value can be written — a manual edit, a CSV import, a raw API call, and an MCP/agent write are all rejected the same way, naming the record that already holds the value. It’s backed by a real database-level unique index underneath the app’s own check, so two concurrent writes can’t both slip through a race the app-level check alone would miss. Turning the setting back off keeps existing data exactly as it is; it only removes the constraint going forward.

Setting it is API/MCP-only today — update_field takes unique and unique_normalize on an existing field; there’s no toggle in the web field editor yet.

Options for select / multi_select / workflow fields are real rows with stable IDs, never inline strings. Records store option ids, which means:

  • Renaming an option is instant and O(1) — every record updates at once.
  • Kanban column order is just option order.
  • Deleting an option is an explicit, counted operation (with an optional “reassign to option X”).

An option can carry an icon (from a curated icon set, not a free-typed emoji) in addition to its colour, and the colour palette is not limited to the classic handful — pick whichever reads right for that value.

A Workflow field is a select in every respect — same coloured options, same storage — with one rule enforced by the server: a database may have at most one. Trying to add a second is refused, naming the existing one, rather than letting two “status” columns drift against each other. Reach for a plain select for any other list of choices; reach for Workflow specifically for the one field a view, an automation, or an agent should treat as the state of a record.

Finding the status columns you already have

Section titled “Finding the status columns you already have”

Workspaces that predate the Workflow field usually have a plain select called “Status” in several databases. There is no per-database hunt: scan the whole workspace and the answer comes back per database, only for databases you can read (the others are simply absent):

  • already has a workflow field (nothing to do),
  • has a candidate, with how confident the match is and why: high when the name (“Status”, “Stage”, “Phase”…) and the options (To do, In progress, Done…) agree; medium when only the options look like a lifecycle; low when only the name does (a red-amber-green column called “Status” is the usual false friend, and it says so),
  • ambiguous: more than one select qualifies, and since a database holds only one workflow field, a person chooses,
  • or nothing looks like a lifecycle.

Converting is a separate step on the ones you pick. It is a dry run by default: it reports what would convert and changes nothing until you ask for the real conversion. It uses the same conversion as editing the field by hand, so option colours, option ids and every record’s value are kept, and each database succeeds or fails on its own (you need creator access on each). Through MCP these are find_workflow_candidates and nominate_workflows; over the API, GET and POST /api/v1/workspaces/{ws}/workflow-nomination.

Converting a person field into a Members relation

Section titled “Converting a person field into a Members relation”

A user field stores a bare user id. An admin can convert one into a real relation to the Members database, so the person is a record you can link, filter and see from the person’s side (“what is assigned to this person”). It is guided and reversible:

  • Dry run first. The default call changes nothing and reports how many records will link, which values cannot be resolved (a user id with no Members row), the field it would create, and, for every guest, whether they would see anything new.
  • Nothing is dropped. Applying keeps the original field (renamed “Assignee (user)”, same api_name, so saved views and automations keep working on it until you repoint them), creates the relation under the original name, and leaves anything it could not resolve on the retained field. List those records with relation is empty AND retained field is not empty.
  • A hidden paired field on Members. Like every relation it has an inverse, created hidden so it does not reshuffle everyone’s Members screen. A second converted field from the same database gets its own.
  • Permission-neutral, and checked. After the change, each guest who cannot read Members is probed through the real read paths; if anything about Members shows up, the conversion is rolled back and names the guest. A guest who cannot read Members sees no chip at all.
  • Reversible. The reverse writes the values back from the relation, restores the name, and removes the relation and its Members-side field. A Members row with no user id has its name parked in a text field. Lookups or rollups built on the relation are listed first and need confirm_dependents.

Admin only; over the API it is POST .../fields/{field}/convert-to-members-relation and .../convert-to-user (both dry_run by default), over MCP convert_user_field_to_members_relation and convert_members_relation_to_user_field.

Every database’s built-in title field starts as free text — you type it, like any other text field. You can switch it to Computed: a template expression, written in the same editor a formula field uses, that’s compiled and materialized into the title on every create and update. Once computed, the title is read-only — direct writes to it are ignored, the same way a formula field ignores a direct write.

The template can reference the record’s own fields, and — through a lookup — a related record’s fields too, so a title can read “Acme — Website Refresh” pulling the client’s name across a relation. It recomputes automatically as the fields it depends on change.

A few field types can pre-fill a new record instead of starting empty, each in the shape that actually stays true over time:

  • Checkbox — “Checked by default on new records.” Two states, so a literal default is the whole story.
  • Date — “Default new records to today” is a flag, not a stored date. A literal default date would be correct for about a day and silently wrong after that; resolving “today” at the moment of insert is the only default that stays accurate.
  • Select / Workflow — a Default option picker in the field’s Edit dialog, with an explicit None to clear a default rather than merely leaving it blank. This one is edit-time only — a field you’re still in the middle of creating has no default-option control yet, because its options are still local drafts without ids to point a default at. Configure the options first, save the field, then set the default from Edit.
  • Rename — the display_name changes freely; the api_name is a stable slug (auto-generated, admin-editable with a warning) so integrations don’t break.
  • Delete — soft delete; orphaned values are ignored by reads and lazily cleaned up, so the action never blocks.
  • Change type — a small compatibility matrix converts in place (anything → text; text → number/date best-effort; select ↔ multi_select). The API returns a dry-run count of lossy conversions before applying. Everything else is an explicit “delete & create new”.

For the full canonical model and JSONB storage mechanics, see the data model reference.