Data model reference

Exhaustive reference for records, schemas, documents, folders, lookups, references, and version history: methods, parameters, field types, validation rules, limits, envelope shape, error codes, and an honest "Notes & limits" for each area.

This page does not reproduce the raw endpoint reference — the full request/response shapes are in the generated API reference (OpenAPI / Scalar). Method names below are the Node SDK sub-client methods. The spec is currently at 0.45.0. PATCH and create-by-typeName-alone require SDK 0.26+ — relevant only if your own integration pins an SDK build older than that; every first-party client is well past it.

Conventions

The list envelope

List, lookup, and version endpoints return a paginated envelope:

{ "data": [ /* items */ ], "nextCursor": "opaque-string-or-null" }

Drain it by feeding the verbatim nextCursor you were given back as startFrom, until nextCursor is null — that is the only end-of-results signal, on every endpoint that returns this envelope. A page can come back short, or even empty, while more results remain; never infer completion from page size or fullness. search.content and usage endpoints are not enveloped (they return their own shapes).

On list and lookup endpoints, the cursor is opaque and encrypted — never construct, parse, or store one long-term; a caller-constructed startFrom is rejected with 400, and a cursor is only valid for the exact query parameters it was issued for. Version history endpoints (get*Versions) resume from a plain row id instead — still echo nextCursor back rather than building your own, but it is not encrypted, and a caller-supplied id is the expected shape there rather than a rejected one.

Page size (limit)

limit is capped, not clamped: on every list/lookup method on this page (listRecords, lookupRecords, listSchemas, listDocuments, listFolders, listUsers, and the app-context/access-profile list endpoints in the identity & access reference) the range is 1–100, defaulting to 20 — a request for limit: 200 is rejected with 400, not silently reduced to 100 and served. The SDK does not clamp or auto-paginate on your behalf either: request more than 100 and the call rejects, exactly as the raw HTTP call would. Page in a loop using nextCursor (above) rather than requesting a single oversized page. (GET /v1/admin/* audit/access-log endpoints are a different family with a wider 1–500 range, default 200 — not covered by this convention.)

Identifiers

typeName, field ids, and lookup field names share one grammar: ^[A-Za-z0-9_-]{1,64}$ — letters (any case), digits, underscore, hyphen; 1–64 characters. camelCase, snake_case, PascalCase, and kebab-case are all valid. The names userId and scopes are reserved and may not be redeclared as lookup fields or payload keys.

Timestamps

Creation and modification timestamps are returned as ISO-8601 UTC strings.

Optimistic concurrency

Records, documents, and folders accept an optional expectedVersion on update/patch. Supply the version you last read; the write is rejected with 409 VERSION_CONFLICT if the entity changed since, leaving it untouched. Omit it for last-write-wins.


Schemas — client.schemas.*

MethodPurpose
createSchema(body)Create a schema.
getSchema({ id })Fetch by id.
updateSchema({ id, body })PUT full-replace (no PATCH).
deleteSchema({ id })Delete.
listSchemas({ startFrom?, limit?, surface? })List (list envelope).
getSchemaVersions({ id })Version history (list envelope).

Schema fields

FieldTypeRequiredNotes
typeNamestringyesImmutable after create. Identifier grammar. Unique within your ownership scope — a different owner may declare a schema under the same typeName via basedOn (see below).
displayNamestringyesHuman-readable name.
descriptionstringno
fieldsFieldDef[]noOmit for a bare schema (no validation).
lookupFieldsLookupDef[]noMax 10 (declared by you); see lookup notes.
renderHintsmap keyed by fieldIdnoUI hints; does not affect storage/validation.
capabilitiesmap<string,boolean>noauditHistory (default true) and triggersEnabled (default false, 0.43.0+ — the opt-in a schema must declare before a trigger rule may fire on its records; declaring a rule against a schema without it is a 400). The two are independent: turning audit history off does not turn triggers off. ⚠️ capabilities is replaced in full when you supply it, so a PUT that sends a partial map to change one flag drops the other — omit the block entirely to preserve it. A PUT that would turn triggersEnabled off while rules still fire off the schema is refused 409, as is a DELETE of the schema.
indexModeenumnoHYBRID | SEMANTIC | TEXT | NONE. Type-level default for instances. Omit = no default.
storageProfileenumnoSTANDARD (default) | LOW_LATENCY | LARGE_PAYLOAD.
allowedSurfacesstring[]yesNon-empty. Any of record, document, user, entity. Identity entities in every namespace — org, client, or one you register — bind under the single entity surface, and the schema's typeName tells them apart. An entity-surface schema is written by an ordinary scoped credential in its own context, same as record/document. A user-surface schema stays root-key only: your users are tenant-global, so their schema has one tenant-wide home. GET /v1/schemas?surface=entity reads the caller's own context and the tenant-wide home together, newest context first, through a single cursor.
activebooleannoDefault true. Inactive schemas reject new record creation.
userId / scopesstring / string[]noOwnership defaults for the schema itself. scopes is <namespace>:<value> entries, max 2.
basedOnstringnoSchema id of the lineage base this schema customizes. The first schema created under a typeName has no basedOn and becomes that name's shared base (must be created by a root/unscoped credential with no userId/scopes); every other schema of that name must declare basedOn, pointing directly at the base (one hop — a variant cannot base off another variant). Immutable once set.

FieldDef

FieldTypeNotes
fieldIdstringRequired. Identifier grammar.
fieldTypeenumstring | number | boolean | date | enum | array | object | reference.
requiredbooleanEnforced on create.
searchablebooleanField text enters the full-text search lane.
filterablebooleanField available as a search filter (no relevance influence).
descriptionstring
validationobjectValidation rules (below).
enumValuesarrayAllowed values for enum fields.
sensitivebooleanRedact-at-write + search-exclusion + read-masking + blind-indexed lookup.
inlinebooleanKeep the field on the record row when the payload is stored out of line (≥ 4 KB, or always on LARGE_PAYLOAD): it appears in list/lookup projections without includePayload, and a trigger rule may project it into input.record. Cannot be combined with sensitive. A schema update that would stop keeping a field inline while a trigger rule still projects it is refused (409); a record or document whose inline fields total more than 224 KB is refused (400).
targetTypeNamestringreference only: the type pointed at (required for references).
targetFieldstringreference only: target lookup field to resolve against (default externalId; must be a unique lookup on the target).
cardinalityenumreference only: one (default) | many.
targetSurfacestringreference only (required for references): surface the target lives on — record, document, user, or the name of an entity-backed namespace (org, client, or one you registered via POST /v1/namespaces). Not a closed enum — the namespace must already be registered and entity-backed, or the schema is rejected at authoring time.

Validation rules (the validation object)

required, minLength, maxLength, min, max, pattern, email, url, phone, step, multipleOf, minItems, maxItems. Rules are enforced at record write; a violation returns a 400 with a readable message before the record is persisted.

64-bit number range (platform-wide, independent of any schema min/max). Every JSON number you write — in any field, whether or not your schema declares it, and in a search request body as well as a payload — must fall within the signed 64-bit range (−9223372036854775808 to 9223372036854775807), carry at most 38 significant digits, and be finite. A value outside that range (or a magnitude below roughly 1e-130) is rejected with a 400 naming the field. Store a large whole number you need to preserve exactly — an external id, an account number, epoch-nanoseconds — in a string field: it keeps the digits byte-exact and still supports exact-match lookup, where a number field would round or reject. (A record written before this rule that holds an out-of-range number may now read back with that field absent or coerced to a string.)

LookupDef

A lookup declares either a single field or a composite of fields — never both, and fieldName is optional on LookupDef precisely because a composite entry omits it:

  • Single-field: a bare field name string, or { fieldName, unique }. unique: true enforces one record per value per tenant+context.
  • Composite (record surface only): { fieldNames: [...] } — two or three field names, in the order they'll be queried; that order is fixed once the schema is live. A composite lookup cannot set unique or rangeEnabled. Code that reads or writes a schema's lookupFields must handle fieldName being absent when fieldNames is present — this is the one breaking shape change for typed clients this release.
  • sortBy — which field this lookup's results are ordered by: createdAt (the default), lastUpdated, or any other declared field (must not be sensitive). This is authored once, on the schema, not chosen per-query — see "Sort order" below for how that interacts with the order query parameter.

RenderHintDef

label, widget (text | textarea | select | date | checkbox), order, section, helpText, displayField (marks the headline field; at most one per schema).

Schema versioning

schemaVersion is a public revision counter: 1 on create, prior + 1 on each update. Records and documents are stamped with the governing schemaVersion at write and keep that value even after the schema evolves. getSchemaVersions returns the immutable version-row history (same envelope and row shape as record versions).

Notes & limits — schemas

  • No PATCH. Schemas are PUT-replace only. Collection fields (fields, lookupFields, renderHints, capabilities) are replaced in full on update — supply the complete intended set; omitted scalar fields are preserved.
  • typeName is immutable after creation.
  • Creating a schema is idempotent by typeName, within your own ownership scope. Re-issuing createSchema for a typeName you already own returns your existing schema rather than failing, so re-running your own provisioning step is safe. To change a schema, update it (PUT-replace).
  • A different owner reusing an existing typeName must declare basedOn. The first schema created under a name becomes that name's shared base (root/unscoped credential only, no userId/scopes); every other owner defining a schema under the same name must set basedOn to the base's schema id, or the create is rejected with a 400. A variant stays the same conceptual type as the base for references, listings, and blueprints, while declaring its own fields/validation.
  • A bare schema runs no payload validation; all string values are still text-indexed for search when its index mode permits.
  • Schema-field reference targets are declarable today; write-time existence/type enforcement of references is not yet active — a reference field carries the link but is not yet validated against the target on write.

Records — client.records.*

MethodPurpose
createRecord(body)Create. typeName and/or schemaId (see below).
getRecord({ id })Fetch by id (always full payload).
updateRecord({ id, body })PUT — replace mutable fields; payload replaced in full.
patchRecord({ id, body })PATCH (RFC 7386). SDK 0.26+.
deleteRecord({ id })Hard delete (+ tombstone).
listRecords({ type, userId?, scope?, startFrom?, limit?, includePayload? })List (list envelope). scope is one <namespace>:<value> filter (e.g. org:<id>) per call.
lookupRecords({ type, field, value?+sortFrom?+sortTo? | values?+sortFrom?+sortTo? | from?+to? | prefix?, order?, startFrom?, limit?, includePayload? })Lookup, one mode (list envelope). For a composite lookup, field is comma-joined and pairs with values (array, order matches the schema's fieldNames). order (asc|desc) is direction only — see Sort order below; the sorted field itself is the schema's sortBy, not a call-time choice.
lookupRecordsByBody({ ... })Body-based lookup (sensitive-safe).
getRecordVersions({ id })Version history (list envelope).
getRecordTombstone({ id })Tombstone for a deleted record.

Create / update fields (RecordRequest)

FieldTypeNotes
typeNamestringThe record type. See type-identification below. Immutable; ignored on update.
schemaIdstringSchema to validate against. See below. Immutable; ignored on update.
payloadobjectValidated against the schema. On PUT, replaces the stored payload in full.
statusstringLifecycle/workflow status (default ACTIVE).
folderIdstringGroup with a folder. Cannot currently be cleared once set.
userIdstringOwnership (Vectros user UUID). Subject to token identity auto-assign.
scopesstring[]Ownership entity edges, each <namespace>:<value> (org:..., client:..., or a namespace you registered) — at most two. On update, an explicit scopes replaces the full set; omit to leave ownership unchanged; [] clears it. Subject to token identity auto-assign.
externalIdstringStable id you assign. Immutable. Unique within tenant+context+typeName (idempotent create). Max 256 chars.
indexModeenumPer-record override: HYBRID/SEMANTIC/TEXT/NONE. Immutable after create.
expectedVersionnumberOptimistic concurrency. Ignored on create.

Type identification (SDK 0.26+ either-or): provide typeName or schemaId (at least one). With only typeName, the server resolves the schema to your own basedOn variant when one exists, otherwise the shared base — typeName is unique within an ownership scope, not tenant+context-wide (see basedOn in the schema fields above). With only schemaId, it resolves the type from the schema. With both, they must agree. An SDK older than 0.26 always sends both.

RecordResponse (selected fields)

id, typeName, schemaId, schemaVersion, externalId, payload, payloadExternalized, payloadBytes, status, folderId, userId, scopes, indexStatus (PENDING_INDEX | INDEXED | SKIPPED | FAILED, null for store-only; SKIPPED = no indexable text, so nothing was indexed — stored + retrievable, not an error), indexFailure (present only when indexStatus is FAILED — an object with a stable code and a human-readable message; see the code table in the operations-trust reference), indexMode, createdBy, createdAt, updatedAt, version.

For an externalized (large) payload, list/lookup responses return only the indexed projection and set payloadExternalized: true; fetch the full payload via by-id GET or pass includePayload: true on the list/lookup call.

Automatic ownership lookups

Every record is automatically lookup-indexed by userId and by scopes without declaring them and without counting against the 10-field cap — so listRecords({ type, userId }) and listRecords({ type, scope: 'org:<id>' }) resolve directly (scope takes one <namespace>:<value> filter per call).

Notes & limits — records

  • PUT replaces the payload in full — it is not deep-merged. Use PATCH (0.26+) for a true partial payload update.
  • PATCH patchable keys: payload, status, folderId, userId, scopes, expectedVersion. Immutable keys (typeName, schemaId, externalId, indexMode) are rejected if present. Within payload, a key set to null is deleted; a top-level patchable field (such as status or folderId) set to null is not a delete — it is rejected with 400. Clearing a top-level field is not supported.
  • typeName is immutable after creation. To change a record's type, write a new record and delete the old one.
  • folderId cannot currently be cleared once set.
  • Delete is hard delete — there is no soft-delete status that lingers in the index.
  • Batch write and batch get are implemented; batch lookup is still reserved. POST /v1/records/batch writes up to 50 records in one call and POST /v1/records/batch-get fetches up to 100 by id. Batch write returns HTTP 200 with a per-item results array even when items failed, so inspect the results rather than the status code. POST /v1/records/lookup/batch is still reserved and returns 501 not_implemented — do not depend on it.

Documents — client.documents.*

MethodPurpose
ingestDocument(body)Inline text ingest.
uploadDocument(body)Request a presigned upload URL (file path).
getDocument({ id })Fetch by id.
getDocumentText({ id })Retrieve the retained text — always available for text-ingested documents; for file uploads unless uploaded with storeText: false.
getDocumentDownloadUrl({ id })Presigned download URL for a file-backed document.
updateDocument({ id, body })PUT — full replace; text re-ingests.
patchDocument({ id, body })PATCH (RFC 7386). SDK 0.26+.
deleteDocument({ id })Hard delete (+ tombstone).
listDocuments({ userId?, scope?, startFrom?, limit? })List (list envelope). scope is one <namespace>:<value> filter per call.
lookupDocuments(...) / lookup-by-bodyLookup on a schema-bound document's lookup fields.
getDocumentVersions({ id })Version history (list envelope).

Upload fields (FileUploadRequest — uploadDocument)

In addition to fileName/fileType/indexMode/ownership/payload/schemaId/externalId:

FieldTypeNotes
storeTextbooleanDefault true: the extracted text is retained — retrievable via getDocumentText and usable by document-ask. Set false to discard the extracted text once indexing completes (search and the original-file download are unaffected; getDocumentText then 404s and document-ask 409s). Fixed at ingest: it cannot be changed later, and a re-upload keeps the original choice.

Ingest / update fields (DocumentRequest)

Text-ingested documents always retain their body (it IS the document) — there is no retention flag on this path.

FieldTypeNotes
titlestringRequired.
textstringInline ingest body. Required on POST ingest; on PUT/PATCH it re-ingests write-through.
indexModeenumHYBRID/SEMANTIC/TEXT/NONE. Optional if the bound schema sets a default; otherwise required. Fixed at creation.
folderIdstringDefaults to the context root. Cannot be cleared once set.
payloadobjectStructured data (records parity). Validated + lookup-indexed when schemaId is set; undeclared keys pass through as free-form, filterable in search. Replaced in full on PUT.
schemaIdstringOptional schema to validate + lookup-index the payload against.
userIdstringOwnership (Vectros user UUID).
scopesstring[]Ownership entity edges, each <namespace>:<value>, at most two. Same replace-on-update semantics as records.
externalIdstringStable id you assign. Immutable. Unique within tenant+context (idempotent ingest). Max 256 chars.
expectedVersionnumberOptimistic concurrency. Ignored on create.

Upload handshake (uploadDocument)

uploadDocument returns uploadUrl, expiresAt (15 minutes), and requiredHeaderName

  • requiredHeaderValue. PUT the raw bytes to uploadUrl without an Authorization header, set Content-Type to the file's MIME type, and set the header named requiredHeaderName to requiredHeaderValue exactly. The URL is single-use, enforced by an S3 conditional-write precondition baked into its signature: this header is part of that signature, so a PUT that omits it (or changes its value) fails with a 403 SignatureDoesNotMatch before reaching this service at all — not this API's JSON error shape. Poll getDocument until status is INDEXED.

DocumentResponse (selected fields)

id, title, externalId, status (PENDING_UPLOAD | UPLOADED | EXTRACTING | PENDING_INDEX | INDEXED | SKIPPED | STORED | FAILED; SKIPPED = extraction produced no indexable text, so nothing was indexed — stored + retrievable, not an error), indexFailure (present only when the status is FAILED — an object with a stable code and a human-readable message; see the code table in the operations-trust reference), indexMode, storeText, folderId, payload, payloadExternalized, schemaId, schemaVersion, textBytes, userId, scopes, fileType, fileSize, createdAt, lastModified, version.

Notes & limits — documents

  • PUT replaces the payload in full; PATCH (0.26+) merges. PATCH patchable keys: title, text, folderId, schemaId, userId, scopes, payload, expectedVersion. indexMode, externalId, and storeText (text retention is fixed at ingest) are immutable and rejected.
  • An update re-runs the indexing pipeline; old content is removed from the index as the new content is written.
  • getDocumentText serves the retained text: always available for text-ingested documents, and for file uploads unless uploaded with storeText: false (which discards the extracted text once indexing completes — the original file stays downloadable). Returns 404 when the text is not retained or extraction has not completed.
  • The presigned PUT must omit the Authorization header — the URL itself carries the grant.

Folders — client.folders.*

MethodPurpose
createFolder(body)Create (optionally under a parent).
getFolder({ id })Fetch by id.
updateFolder({ id, body })PUT — name/description/ownership.
patchFolder({ id, body })PATCH (RFC 7386). SDK 0.26+.
deleteFolder({ id })Delete (rejects non-empty).
listFolders({ userId?, scope?, startFrom?, limit? })List (list envelope). scope is one <namespace>:<value> filter per call.
getFolderVersions({ id })Version history (list envelope).

Create / update fields (FolderRequest)

FieldTypeNotes
namestringRequired.
descriptionstringOptional.
parentFolderIdstringApplied at create only; ignored on update. Omit to create under the context root.
slugstringStable, sibling-unique slug; derived from the name when omitted. Lowercase letters/digits/hyphens. Immutable.
userIdstringOwnership (Vectros user UUID).
scopesstring[]Ownership entity edges, each <namespace>:<value>, at most two.
expectedVersionnumberOptimistic concurrency. Ignored on create.

FolderResponse (selected fields)

id, name, description, parentFolderId (null only for a true root), slug, depth (0 at root), isProtected, userId, scopes, createdAt, lastModified, version.

Notes & limits — folders

  • No move / reparent. parentFolderId is fixed at creation; there is no operation to relocate a folder in the hierarchy.
  • Delete rejects a non-empty folder with a 400 — remove children first. "Non-empty" means ANY of the three things a folder can hold: sub-folders, documents (file-backed and text-ingested), and records. Enumerate them with GET /v1/folders?parentFolderId=, GET /v1/documents?folderId= and GET /v1/records?folderId=. The guard counts items your own credential may not be able to read, so a folder in a shared tenant can refuse to delete for a narrowly-scoped credential without that credential being able to see why. Emptying it then needs a credential whose reach covers the whole folder.
  • The context root folder is protected (isProtected: true) and created lazily on first folder interaction. Unparented folders are placed under it, so a folder created without a parent still has a non-null parentFolderId (the root's id).
  • PATCH patchable keys: name, description, userId, scopes, expectedVersion. slug and parentFolderId are immutable and rejected.

Scripts — client.scripts.* and POST /v1/scripts/execute

A script is a stored, versioned JavaScript program (POST /v1/scripts pushes an immutable new version of a name; GET/DELETE /v1/scripts/{id} manage versions) that the platform runs for you in a sandbox with the vectros.records, vectros.documents and vectros.folders host functions. Two things run a script: a trigger rule (asynchronously, when a record event fires it) and POST /v1/scripts/execute (synchronously, as one atomic call).

Synchronous execution (POST /v1/scripts/execute)

FieldTypeNotes
scriptRef.namestringThe script's name.
scriptRef.versionstringA version number ("3") or "latest", resolved at request time.
inputobjectFree-form JSON your script receives as input.params. Optional; {} when absent.

Response: { result, execution: { id, durationMs } }. result is whatever the script returned, converted as JSON.stringify would; the platform makes no promise about its shape. It is returned only after the script's writes have committed — on any failure the body is the standard error envelope and nothing the script staged is committed: rows are written only when the transaction commits, and a document body or a large payload is decided at that point and written to storage only after the commit, so an aborted execution leaves nothing behind. (The one residual sits after a successful commit, outside the response: if storing a body fails then, the row exists and the body does not until the platform's repair lands — a 200 guarantees the rows; read a document's text before relying on it.)

Permission. scripts:x — the permission to execute — separate from scripts:c (push). Bare scripts:x grants every script in the context; scripts:x:<name> grants exactly that name, every version. Execution discloses no source (scripts:r is not needed), and scripts:x alone can read or write nothing: inside the script every host call is enforced by your credential's data scopes exactly as the equivalent REST call would be.

What the script sees. input.event is the literal "execute", input.userId is your credential's user id (or null), and input.params is your input object verbatim. That is the same input grammar a trigger rule uses, so one script can serve both by branching on input.event; input.schemaId, input.recordId and input.scope are absent on this path. params is not validated: a script's declaredInputContract is documentation for readers, not an enforced schema — a script that needs a shape checks for it and throws, which reaches you as a 400 SCRIPT_ERROR carrying the script's own message.

Trigger execution — what the script sees

A rule fires on a record event and the script receives the record as an image, not a pointer:

KeyPresent onValue
input.eventall"CREATE", "UPDATE" or "DELETE".
input.schemaIdallThe schema the rule fires off.
input.recordIdallThe record's id — what vectros.records.get accepts.
input.userId, input.scope.<ns>allThe record's stamped ownership (the values the rule's ${{ input.* }} grant resolved against).
input.recordallThe rule's declared fields, from the row as written — on DELETE, from the deleted row (the only way a script can see it). Absent only for a firing dispatched before this release rolled out.
input.previousUPDATEThe same fields from the row before the write.

fields is required when you declare the rule (POST /v1/triggers), and [] projects nothing beyond the identity. Every entry must be a field the schema keeps inline — inline: true, filterable, or a lookup field — and not sensitive; the rule is rejected with a 400 listing the projectable fields otherwise. A rule that declares fields must also be able to read every record it fires on: its grant must hold a records:r clause covering the schema's type (a grant with no read at all is rejected), and that clause may not carry a literal data_scope constraint (a ${{ input.<dim> }} placeholder is fine — it resolves to the firing record) or the rule is rejected; and if at execution the rule's grant cannot read the firing record, record and previous are delivered as empty objects. The reason is the storage tier: a payload moves out of the row above 4 KB, only the inline fields ride the change event a trigger fires from, and a field outside that set would reach a script for small records and silently vanish for large ones. A field the record does not carry is undefined, never null. record plus previous are capped at 224 KB serialised; an over-cap firing is not run and is recorded as an INPUT_TOO_LARGE failure naming the size. A record last written before its schema inlined a field may lack it in previous (or in record on DELETE) until it is written again.

Reads inside the script. A get sees the script's own staged writes. A list, query or lookup never shows a row the script has deleted, shows a row it re-saved with the updated content, and does not show a row it created — you hold that row's id from create(). (The same holds inside a trigger execution.)

Budget — "seconds of useful work". 15 s of wall clock (never more than 16), a bounded number of statements and of host calls (a runaway script is stopped early with RESOURCE_LIMIT_EXCEEDED), and one transaction of at most 100 storage rows after merging — stated in rows because that is the storage transaction's own limit, not a platform choice:

OperationRows
folders.create / folders.delete3
documents.create (text)1 + one per range-indexed lookup field + one per reference field
records.create1 + range-indexed fields + references (+ over-budget fallback rows)
any update via a full savethe same as its create

So "create a folder and file N plain text documents" holds up to 97 documents (48 with one range field or one reference each, 32 with both). Over the limit the error is 400 WRITE_BUFFER_CAP_EXCEEDED with total and limit, and nothing is committed.

Limits and metering. The request takes the write-path limits once, before the script runs — burst, monthly credit ceiling, principal quota — with no free-read allowance, so an account over its credit ceiling is refused (402) even for a read-only script. Each read the script performs is metered as a read and each write as a write; execution time beyond what those operations include is charged as script execution time (see GET /v1/usage, whose execution section covers both trigger and synchronous scripts). Concurrent executions per app are bounded; at the limit the response is 429 with Retry-After: 2.

Retries — the Idempotency-Key header. Optional, 1–128 characters of letters, digits, ., _, : or -. A request repeated under the same key within 24 hours receives the same response as the first attempt without executing again; a duplicate still in flight is 409 IDEMPOTENCY_IN_PROGRESS; the same key on a different request is 422 IDEMPOTENCY_KEY_REUSED. If an execution's outcome cannot be determined — the script did not finish inside the budget while its writes may have committed — the response is 500 EXECUTION_OUTCOME_UNKNOWN carrying execution.id, and a keyed retry receives that same answer: inspect what the script would have written, then use a new key for a new attempt. With a key present the serialised result is capped at 16 KB (256 KB otherwise): a retry-safe script returns ids, not payloads. Without a key, a lost response cannot be distinguished from a failed one — re-read before retrying.

Errors (standard envelope, errorCode named): 400 SCRIPT_ERROR · RESOURCE_LIMIT_EXCEEDED · WRITE_BUFFER_CAP_EXCEEDED · RESULT_TOO_LARGE; 403 missing scripts:x · AUTHORIZATION_DENIED; 404 unknown script; 409 CONCURRENT_MODIFICATION (names the ids to re-read; never a version) · IDEMPOTENCY_IN_PROGRESS; 413 body over 256 KB; 422 IDEMPOTENCY_KEY_REUSED; 429 burst / concurrency / probe budget; 500 EXECUTION_OUTCOME_UNKNOWN or an internal error with a correlationId; 503 writes frozen · EXECUTION_NOT_STARTED · EXECUTION_CONTAINER_BUSY (retry after Retry-After); 504 TIMEOUT.

Lookups & references

Lookup modes

Exactly one mode per call:

ModeParametersConstraints
exactvalue (single field), or values (composite — array, order matches fieldNames)Any lookup field. A single-element values means the same as value. Sensitive fields must use the body variant. May be narrowed with sortFrom/sortTo (below).
rangefrom + toBoth required. Inclusive, ascending. Non-sensitive, single-field lookups only.
prefixprefixString, non-sensitive, single-field lookups only. Ascending.

Supplying zero or more than one mode is a 400. Range with only one bound is a 400. Prefix on a non-string field is a 400.

Composite lookups (record surface only)

A LookupDef can name two or three fields together via fieldNames instead of a single fieldName. Query it with a comma-joined field (field: 'status,area') and a matching values array in the same order — repeated query params on GET, an array in the body on POST. Field order is fixed at authoring time and cannot be changed later; you may query a leading run of the declared fields (the first field alone, the first two, and so on) but never a later field on its own — declare a separate lookup for that. Supplying fewer values than declared returns records grouped by the unspecified fields; a requested sort order then applies within each group, not across the whole result. A composite lookup cannot be unique or rangeEnabled, and its schema's allowedSurfaces must be record only.

Sort-key window (sortFrom / sortTo)

An exact-value lookup accepts optional sortFrom/sortTo bounds on the lookup field's own sort key — inclusive, either given alone, expressed in that field's own units (epoch milliseconds for a timestamp field such as createdAt/lastUpdated). On a composite lookup, a sort-key window requires the full tuple of values; it is not available on a grouped/partial query. Records with no value on the sorted field sort ahead of records that have one, and are never included in a bounded window. The result still pages with the standard { data, nextCursor } envelope.

Sort order (order) vs. sort field (sortBy)

An enumeration lookup accepts an order parameter — asc (default) or desc — as a query param on GET, or the order option on the SDK's lookupRecords. order picks direction only. Which field the results are ordered by is not a per-query choice — it is the lookup's sortBy, declared once on the schema (see LookupDef above; defaults to createdAt). Passing a sortBy query parameter here has no effect — it is silently ignored, not rejected — because the field it would need to change is fixed by the schema, not the request. If you need results ordered by a different field, change the schema's sortBy for that lookup (or declare a second lookup field with a different sortBy); don't guess a sortBy query parameter and assume it took effect. (Unrelated same-named field: RenderHintDef.order above is a form-builder display position, not a query sort — don't confuse the two.)

Unique vs non-unique (enumeration)

  • A unique lookup field returns at most one record and is enforced unique on write.
  • A non-unique lookup field is an enumeration — it returns every record sharing the value, paginated via the { data, nextCursor } envelope.

Sensitive-field lookup (body variant)

For a sensitive field, the exact value must not travel in a URL. The GET lookup rejects value on a sensitive field and directs you to the body-based variant, where the value travels in the request body and is blind-indexed server-side. Range and prefix are not available on sensitive fields (a blind-indexed value has no usable order).

References

A reference field links to another record: it declares targetTypeName (required), targetField (default externalId; must be a unique lookup on the target), cardinality (one/many), and targetSurface. The platform can additionally maintain per-field reverse-reference rows (opt-in) to index the inverse direction.

Notes & limits — lookups & references

  • Lookup fields you declare are capped at 10 per schema (the three automatic ownership lookups do not count).
  • The reverse-reference list endpoint is not yet available — you cannot query back-references through the API today.
  • Reference targets are declarable now; write-time existence/type enforcement of a reference against its target is not yet active.

Version history — getRecordVersions / getDocumentVersions / getSchemaVersions / getFolderVersions

Each returns the { data, nextCursor } list envelope of immutable version rows for an audited entity.

Version-row fields

FieldTypeNotes
idstringVersion-row id.
changeTypeenumCREATE | UPDATE | DELETE.
previousContentstringJSON-stringified snapshot of the state prior to this change. Populated on UPDATE/DELETE; null on CREATE. JSON.parse to inspect.
previousVersionnumberVersion number before this change; null on CREATE.
changeReasonstringCaller-supplied or system-derived reason.
changedBystringId of the user / key responsible.
createdAtstringISO-8601 timestamp of the row.
changedFieldsobjectField-level diff: summary, changed field names, count, per-field old→new detail. Null on CREATE.

Audit capability & retention

  • Audit history is governed per schema by capabilities.auditHistory (default true). Setting it false stops recording the change-history trail for that type's data; it never deletes or affects the records/documents themselves. Tombstones on delete are recorded regardless.
  • Version rows are written asynchronously (typically 1–3 seconds after a write returns) — poll briefly if you read history immediately after a write.
  • Audit and version history are retained compliantly; heavy historical content is externalized to a write-once, retention-governed store, and the history forms a tamper-evident continuity chain. The full retention and integrity posture lives in ../operations-trust/compliance.md.

Errors

The platform returns a uniform error contract. Common cases for the data model:

StatusMeaning (data-model context)
400Validation error: schema-field violation, bad identifier, multiple/zero lookup modes, range/prefix on a sensitive field, prefix on a non-string field, delete of a non-empty folder, immutable field present in a PATCH, a top-level field set to null in a PATCH, a number outside signed 64-bit range / with more than 38 significant digits / non-finite, a caller-constructed (non-verbatim) startFrom, or a startFrom used with different query parameters than it was issued for (the error names the field, where applicable).
404 / "not found"The entity does not exist, or belongs to another tenant/context, or is out of the caller's token scope — a single uniform shape (the message never distinguishes these).
409 VERSION_CONFLICTexpectedVersion did not match — the entity changed since you read it; it is left untouched.
501 not_implementedA reserved-but-unbuilt operation (batch record lookup).

Where to go next