Records

Creating types, publishing, writing rows, value formats and errors.

Raw Markdown for agents: records.md. MCP: read_guide("records").

Records are the structured data of a Space: user-defined record types (like Supplier or Invoice) with typed fields, relations between types and a full revision history per record. Documents are evidence, records are the state.

Flow: from a new type to rows

  1. records.types.create with name, plural_name, an optional key and the fields in fields. The required title field name is created automatically. All fields are validated together; one invalid entry rejects the whole call.
  2. Optionally records.fields.add_many to add more fields to the draft in one call (records.fields.add adds a single one). Relation fields are created with records.relationships.create.
  3. records.types.publish_preview lists every change with severity safe/warning/blocked. Fix blocked changes in the draft.
  4. records.types.publish turns the draft into an immutable schema version and creates the table. It needs space administration rights; in the chat it runs after the user confirms it.
  5. records.types.describe returns the field keys, option keys and an example payload; then write with records.rows.create or records.rows.upsert_many.

Draft revisions: every draft change (types.update, fields., relationships.) raises the type's draft_revision by one and returns the new value. Pass the last value you saw as expected_draft_revision; if someone else changed the draft in between, the call fails with draft_revision_conflict instead of overwriting their change. Omit it only when no one else edits the type.

records.fields.add, records.fields.add_many, records.fields.update and records.fields.reorder answer {field or fields, draft_revision, field_count}. Pass include_type: true to get the full type detail with every draft field, relationship and the published schema instead; records.types.get returns the same detail at any time.

Rules

  • Keys match ^[a-z][a-z0-9_]{0,63}$: a lowercase letter, then lowercase letters, digits or underscores, at most 64 characters.
  • Reserved field keys: archived_at, created_at, created_by, id, revision, title, updated_at, updated_by. These system columns exist on every record and are returned automatically; choose another key.
  • A record type holds at most 150 fields including name. A space holds at most 100 record types.
  • single_select and multi_select fields need config.options with at least one {id, key, label}; the id may equal the key. unique is allowed for text, number, date and datetime fields.
  • Values by field type when writing rows:
    • text, long_text: a string.
    • number: a decimal string such as "12.50".
    • money: an object {"amount": "12.50", "currency": "COP"}. amount is a decimal string, currency an ISO 4217 code; without currency the field's currency_default is used. A bare number is rejected.
    • boolean: true or false.
    • date: "YYYY-MM-DD". datetime: ISO 8601, stored and returned in UTC.
    • single_select: one option key. multi_select: a list of option keys.
    • record_relation: in relations as {field_key: [record ids]}; document_relation: in documents as {field_key: [document ids]}. Both replace the full list.
    • lookup and count are computed and read-only.
  • records.rows.upsert_many writes up to 200 rows atomically. merge_on names unique field keys: no match creates, one match updates, several matches fail and the whole batch rolls back. dry_run returns the plan without writing. Runs started by skills or workflows may touch at most 5000 records.

Reading

records.types.list lists the types of a Space with status and record counts; records.types.describe works for published types and, with published: false, for drafts, so you can check the example payload before publishing. records.types.schema_graph returns every type with its relationships.

records.rows.query lists records of a type. Pass a filter tree {"combinator": "and", "conditions": [{"field": "status", "op": "eq", "value": "open"}]}, optional sort [{"field": "issued", "direction": "desc"}], q for a text search, view_key to start from a saved view, group_by for counts and the signed cursor from the previous page to continue. records.rows.get reads one record; records.rows.history pages through its revisions. records.rows.linked_to_document lists the records that reference a document.

Large types: for types with many rows start with records.charts.data and an inline spec for sums, counts and trends (guide records_charts), then read single records with records.rows.query and a filter. Never page through a large type unfiltered.

Updating and archiving

records.rows.update needs expected_revision from the last read plus set, unset, relations or documents. Writes may pass schema_version; schema_changed means the type was published again, call records.types.describe again. records.rows.archive is reversible with records.rows.restore; pass unlink_relations true to drop all links of the record as well (restore does not bring them back). records.rows.restore_revision brings back an older revision as a new one.

Human decisions: records.rows.bulk_import and records.rows.archive_many run in the chat only after the user approves the card; MCP clients and skill or workflow runs get 403 human_only for them. Permanent deletion (records.types.purge, records.rows.purge) and snapshot restores stay with the user in the app.

Conversions: publishing a field type change first stores a PRE_CONVERSION snapshot (records.snapshots.list); preview it with records.fields.convert_preview. A publish that converts a field of a type with more than 100000 rows answers {"queued": true, "job_id": ...}; poll records.jobs.get until the status is SUCCEEDED or FAILED.

Errors and fixes

  • invalid_field_value: details.field names the value. A reserved key lists details.reserved, a malformed key shows details.pattern. Pick another key.
  • validation_error from records.types.create or records.fields.add_many: details maps each invalid entry (for example fields[3].key) to its problem. Nothing was created; fix those entries and send the whole call again.
  • draft_revision_conflict: the draft changed since you read it. details.draft_revision is the current value; re-read with records.types.get if needed and retry with it.
  • record_type_not_published: the type is still a draft. Run records.types.publish_preview, then records.types.publish.
  • schema_incompatible on publish: blocked changes remain; details.changes lists them (the approval card shows them as publish_blocked). Fix the draft and publish again.
  • revision_conflict on a row: details.current carries the current row; read it and retry with its revision.
  • unknown_field: call records.types.describe for the current keys.
  • required_field_missing, unique_violation, invalid_filter, invalid_cursor, record_archived and limit_exceeded name the field, filter or limit.

Example

Create a type with three fields:

{
  "operation": "records.types.create",
  "arguments": {
    "space_id": "<space uuid>",
    "name": "Invoice",
    "plural_name": "Invoices",
    "key": "invoice",
    "fields": [
      {"key": "code", "label": "Code", "type": "text", "unique": true},
      {"key": "total", "label": "Total", "type": "money",
       "config": {"currency_default": "COP", "precision": 2}},
      {"key": "status", "label": "Status", "type": "single_select",
       "config": {"options": [
         {"id": "open", "key": "open", "label": "Open"},
         {"id": "paid", "key": "paid", "label": "Paid"}
       ]}}
    ]
  }
}

After records.types.publish_preview and records.types.publish, write rows:

{
  "operation": "records.rows.upsert_many",
  "arguments": {
    "record_type_id": "<record type uuid>",
    "merge_on": ["code"],
    "rows": [
      {"values": {"name": "INV-0042", "code": "inv_0042",
                  "total": {"amount": "1250000.00", "currency": "COP"},
                  "status": "open"}},
      {"values": {"name": "INV-0043", "code": "inv_0043",
                  "total": {"amount": "98000.00"}, "status": "paid"}}
    ]
  }
}

Auf dieser Seite