Database Records
Read and write records in any database, including field values, files, colors, deletion, and restoration.
A workspace can contain several databases. A record can belong to more than one database, and it does not need a list. Use explicit databaseId values when your integration works across databases.
These GraphQL operations are available to integrations with the required workspace, database, and field permissions. The sync* mutations support durable client writes and require their documented protocol versions, sequences, and replay handling. For a plain field update, setRecordFieldValue needs none of that sync metadata.
Find databases, records, and fields
Use GraphQL to discover databases, records, and visible fields:
query ListDatabases($workspaceId: ID!) {
recordDatabases(workspaceId: $workspaceId) { id name }
}query ListDatabaseRecords($databaseId: ID!, $after: String) {
recordDatabaseRows(input: { databaseId: $databaseId, first: 50, after: $after }) {
rows {
record { id createdAt updatedAt }
databaseId
fields { id name type value editable }
}
nextCursor
hasMore
}
}Pass nextCursor as after for the next page. This GraphQL input uses after; the REST wrapper calls the same parameter cursor.
query ListDatabaseFields($workspaceId: ID!, $databaseId: ID!) {
recordDatabaseFields(workspaceId: $workspaceId, databaseId: $databaseId, forView: true) {
id
name
type
options { id title color }
}
}forView: true returns caller-visible fields and option choices. Without it, the schema-editor projection requires field-management access and may omit options. Use returned field IDs and option IDs in writes.
The same discovery operations are also available through REST database records. You do not need to switch to REST to list databases, records, or fields.
Read one record
query ReadDatabaseRecord($id: String!, $databaseId: ID!) {
record(id: $id, databaseId: $databaseId) {
id
title
customFields { id name value }
list { id title }
}
}The selected database determines the title and fields. Field permissions still apply. list can be null for a listless record.
todo(id, databaseId) is the legacy alias. Without a database ID, reads preserve the workspace’s migrated database home when the record belongs to it; otherwise they use its sole live home in that workspace. Multiple native homes require an explicit database ID. An invalid explicit database never falls back to another home.
You can also use list queries filtered by record IDs. You do not need to use a list query solely to read one record.
Write field values
setRecordFieldValue is the public online GraphQL writer. It returns true on success and a GraphQL error if the write cannot be applied. It requires no protocol version, mutation ID, or sequence number.
mutation WriteDatabaseField($input: SetRecordFieldValueInput!) {
setRecordFieldValue(input: $input)
}Variables for a text field:
{
"input": {
"workspaceId": "workspace-id",
"databaseId": "database-id",
"recordId": "record-id",
"fieldId": "field-id",
"kind": "text",
"value": { "text": "Updated value" }
}
}Use lowercase write kinds, not the uppercase field type names:
| Field type | kind | value example |
|---|---|---|
| TEXT_SINGLE | text | {"text":"Example"} |
| TEXT_MULTI | text | {"html":"<p>Example</p>"} |
| SELECT_SINGLE | select | ["option-id"] |
| SELECT_MULTI | select | ["first-option-id","second-option-id"] |
| REFERENCE | reference | ["target-record-id"] |
| FILE | file | ["file-id"] |
| ASSIGNEE | assignee | ["user-id"] |
| CHECKBOX | checkbox | true |
| NUMBER | number | {"number":42} |
| DATE | date | {"start":"2026-10-15","end":null,"timezone":"UTC","granularity":"ALL_DAY"} |
Array values replace the whole set. Send [] to clear a selection, reference, assignee set, or file set. Select IDs must belong to the field; references and files must satisfy the field’s access and scope rules. FILE values use File.id, not File.uid.
The existing setRecordCustomField / setTodoCustomField mutations also accept databaseId for supported field values. Their type-specific input parameters remain valid.
Attach a file without replacing other attachments
Complete the upload flow first, then attach the returned file UID:
mutation AttachDatabaseFile {
createRecordCustomFieldFile(input: {
todoId: "record-id"
databaseId: "database-id"
customFieldId: "file-field-id"
fileUid: "uploaded-file-uid"
})
}This adds one attachment. Its legacy alias is createTodoCustomFieldFile. Unlike a whole-set kind: "file" write, this operation takes the file UID and preserves other attachments.
Create, edit, and copy
Use createRecordInDatabase for explicit database creation:
mutation CreateDatabaseRecord {
createRecordInDatabase(input: {
workspaceId: "workspace-id"
databaseId: "database-id"
title: "New record"
}) { recordId databaseId created }
}Optional groupOptionId selects a live option on a mounted single-select field. fieldValues accepts {fieldId, kind, value} objects. createRecord / createTodo retain their workspace/list creation contract.
For title and color edits, pass databaseId to editRecord:
mutation SetDatabaseRecordColor {
editRecord(input: {
todoId: "record-id"
databaseId: "database-id"
color: "#ff4b4b"
}) { id color }
}Colors require a valid hex string and follow the database’s color field and permissions. Titles follow its display field; a name formula can prevent direct title edits. copyRecord accepts sourceDatabaseId to select the source home and retains its destination-list contract.
Legacy assignee, date, tag, and bulk operations require the database’s corresponding legacy field bindings. An ordinary field of a similar type is not automatically that built-in field. For native database fields, use their field IDs with setRecordFieldValue. Separate field calls are not an atomic replacement for a bulk mutation.
Delete and restore
mutation DeleteDatabaseRecord {
deleteRecord(input: { todoId: "record-id", databaseId: "database-id" }) {
success
}
}mutation RestoreDatabaseRecord {
restoreRecord(id: "record-id", databaseId: "database-id") { id }
}Deletion moves the record to Trash; it does not permanently purge it. Restore requires Trash permissions, with a short undo exception for the original deleter while they still have delete permission. A record deleted with its list must be restored through that list. Archive is separate from deletion.
Move a database to another workspace
moveRecordDatabase moves a database, with its records, fields, values, comments, files, references, menu items, forms, automations, and views, to another workspace of the same organization. Records, fields, and values keep their IDs, so existing integrations keep working.
Call moveRecordDatabasePreview first with the same input. It writes nothing and reports what the move would change.
query PreviewMove {
moveRecordDatabasePreview(input: {
databaseId: "database-id"
destinationWorkspaceId: "workspace-id"
bringRoles: true
bringMembers: true
}) {
blockers { code message }
sharedFields { fieldId name canFork canPromote }
counts { records tabs forms automations savedViews publicViews }
members { gaining { userId } added { userId } losing { userId } }
}
}A non-empty blockers list means the move is refused. Codes are MEMBER_ROLE, SAME_WORKSPACE, MIRRORED, GITHUB_LINK, SHARED_FIELD, PROMOTE_AUTHORITY, and PDF_TEMPLATE, each with a message.
mutation MoveDatabase {
moveRecordDatabase(input: {
databaseId: "database-id"
destinationWorkspaceId: "workspace-id"
bringRoles: true
bringMembers: true
}) {
moved
sourceWorkspaceId
destinationWorkspaceId
rolesCreated
rolesReused
membersAdded
forkedFields { sourceFieldId destinationFieldId }
promotedFieldIds
}
}| Input | Type | Description |
|---|---|---|
databaseId | ID! | The database to move. |
destinationWorkspaceId | ID! | A live workspace of the same organization. |
bringRoles | Boolean | Create, or reuse by exact name, a destination role for each custom role that governs the database, with its field permissions. |
bringMembers | Boolean | Add the source members the destination lacks, at the same level. Existing destination memberships never change. |
sharedFieldChoices | [MoveRecordDatabaseSharedFieldChoiceInput!] | One {fieldId, choice} per field the database shares with another database of the source workspace. choice is FORK or PROMOTE. |
A field cannot belong to two workspaces. FORK gives the moved database its own copy with the same definition, options, values, and history, and leaves the original with the other databases. PROMOTE makes one organization-level field that keeps serving every database, and requires organization field-management rights. Omit sharedFieldChoices to default to FORK where it is possible, otherwise PROMOTE.
moved is false when the database was already in the destination workspace. Both operations require OWNER or ADMIN authority over the source workspace and the destination workspace.
Bulk edits
updateRecords applies one change to many records. startBulkEdit takes the same UpdateRecordsInput and reports how it ran:
- 100 records or fewer: Blue writes them before the mutation returns, all or nothing, and returns
null. - More than 100 records: Blue saves the request and the record IDs, returns a
BulkEditJob, and a worker writes the records in batches of 50, one transaction each. One job covers up to 50,000 records.
mutation StartBulkEdit($input: UpdateRecordsInput!) {
startBulkEdit(input: $input) { id status totalCount }
}Poll bulkEditJob(id:) for progress, or bulkEditJobs(limit:) for the caller’s recent jobs in the workspace, newest first.
query BulkEditProgress($id: ID!) {
bulkEditJob(id: $id) {
status
totalCount
succeededCount
failedCount
skippedCount
canRetryFailed
canUndo
startedAt
finishedAt
}
}status is QUEUED, RUNNING, SUCCEEDED, PARTIAL (finished with at least one failed record), or FAILED (no record was written).
| Operation | GraphQL | Description |
|---|---|---|
| Read failures | bulkEditJobFailures(id: ID!, first: Int = 100): [BulkEditJobFailure!]! | The records that failed, each with a recordId and a client-safe reason. |
| Retry failures | retryFailedBulkEdit(id: ID!): BulkEditJob! | Run only the failed records again, with the saved request and IDs. Needs canRetryFailed. |
| Undo an edit | undoBulkEdit(id: ID!): BulkEditJob! | Return the records to their previous values. Needs canUndo. |
canUndo stays true for a finished edit for 7 days, and each edit can be undone once. An undo job has kind: UNDO: its skippedCount counts the records left as they are because they changed after the edit, and undoOfJobId points at the edit it reverses.
TABLE fields
Pass the same databaseId when reading todoTableRows, reading todoCustomFieldVersion, and saving with setTodoTableRows. Read the current version before saving; a stale version is refused to prevent overwriting another edit. TABLE row writes use these dedicated operations, not setRecordFieldValue.