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.38.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.
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.*
| Method | Purpose |
|---|---|
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
| Field | Type | Required | Notes |
|---|---|---|---|
typeName | string | yes | Immutable after create. Identifier grammar. Unique within your ownership scope — a different owner may declare a schema under the same typeName via basedOn (see below). |
displayName | string | yes | Human-readable name. |
description | string | no | |
fields | FieldDef[] | no | Omit for a bare schema (no validation). |
lookupFields | LookupDef[] | no | Max 10 (partner-declared); see lookup notes. |
renderHints | map keyed by fieldId | no | UI hints; does not affect storage/validation. |
capabilities | map<string,boolean> | no | auditHistory (default true). |
indexMode | enum | no | HYBRID | SEMANTIC | TEXT | NONE. Type-level default for instances. Omit = no default. |
storageProfile | enum | no | STANDARD (default) | LOW_LATENCY | LARGE_PAYLOAD. |
allowedSurfaces | string[] | yes | Non-empty. Any of record, document, user, entity. Identity entities in every namespace — org, client, or one you register — bind under the single entity surface. |
active | boolean | no | Default true. Inactive schemas reject new record creation. |
userId / scopes | string / string[] | no | Ownership defaults for the schema itself. scopes is <namespace>:<value> entries, max 2. |
basedOn | string | no | Schema 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
| Field | Type | Notes |
|---|---|---|
fieldId | string | Required. Identifier grammar. |
fieldType | enum | string | number | boolean | date | enum | array | object | reference. |
required | boolean | Enforced on create. |
searchable | boolean | Field text enters the full-text search lane. |
filterable | boolean | Field available as a search filter (no relevance influence). |
description | string | |
validation | object | Validation rules (below). |
enumValues | array | Allowed values for enum fields. |
sensitive | boolean | Redact-at-write + search-exclusion + read-masking + blind-indexed lookup. |
targetTypeName | string | reference only: the type pointed at (required for references). |
targetField | string | reference only: target lookup field to resolve against (default externalId; must be a unique lookup on the target). |
cardinality | enum | reference only: one (default) | many. |
targetSurface | string | reference 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: trueenforces one record per value per tenant+context. - Composite (
recordsurface 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 setuniqueorrangeEnabled. Code that reads or writes a schema'slookupFieldsmust handlefieldNamebeing absent whenfieldNamesis present — this is the one breaking shape change for typed clients this release.
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. typeNameis immutable after creation.- Creating a schema is idempotent by
typeName, within your own ownership scope. Re-issuingcreateSchemafor atypeNameyou 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
typeNamemust declarebasedOn. The first schema created under a name becomes that name's shared base (root/unscoped credential only, nouserId/scopes); every other owner defining a schema under the same name must setbasedOnto the base's schema id, or the create is rejected with a400. 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
referencefield carries the link but is not yet validated against the target on write.
Records — client.records.*
| Method | Purpose |
|---|---|
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?, 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). |
lookupRecordsByBody({ ... }) | Body-based lookup (sensitive-safe). |
getRecordVersions({ id }) | Version history (list envelope). |
getRecordTombstone({ id }) | Tombstone for a deleted record. |
Create / update fields (RecordRequest)
| Field | Type | Notes |
|---|---|---|
typeName | string | The record type. See type-identification below. Immutable; ignored on update. |
schemaId | string | Schema to validate against. See below. Immutable; ignored on update. |
payload | object | Validated against the schema. On PUT, replaces the stored payload in full. |
status | string | Lifecycle/workflow status (default ACTIVE). |
folderId | string | Group with a folder. Cannot currently be cleared once set. |
userId | string | Ownership (Vectros user UUID). Subject to token identity auto-assign. |
scopes | string[] | 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. |
externalId | string | Stable partner id. Immutable. Unique within tenant+context+typeName (idempotent create). Max 256 chars. |
indexMode | enum | Per-record override: HYBRID/SEMANTIC/TEXT/NONE. Immutable after create. |
expectedVersion | number | Optimistic 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. Withinpayload, a key set tonullis deleted; a top-level patchable field (such asstatusorfolderId) set tonullis not a delete — it is rejected with400. Clearing a top-level field is not supported. typeNameis immutable after creation. To change a record's type, write a new record and delete the old one.folderIdcannot currently be cleared once set.- Delete is hard delete — there is no soft-delete status that lingers in the index.
- Batch write / lookup / get are reserved and not yet implemented (they return a
501 not_implemented). Do not depend on them.
Documents — client.documents.*
| Method | Purpose |
|---|---|
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-body | Lookup 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:
| Field | Type | Notes |
|---|---|---|
storeText | boolean | Default 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.
| Field | Type | Notes |
|---|---|---|
title | string | Required. |
text | string | Inline ingest body. Required on POST ingest; on PUT/PATCH it re-ingests write-through. |
indexMode | enum | HYBRID/SEMANTIC/TEXT/NONE. Optional if the bound schema sets a default; otherwise required. Fixed at creation. |
folderId | string | Defaults to the context root. Cannot be cleared once set. |
payload | object | Structured 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. |
schemaId | string | Optional schema to validate + lookup-index the payload against. |
userId | string | Ownership (Vectros user UUID). |
scopes | string[] | Ownership entity edges, each <namespace>:<value>, at most two. Same replace-on-update semantics as records. |
externalId | string | Stable partner id. Immutable. Unique within tenant+context (idempotent ingest). Max 256 chars. |
expectedVersion | number | Optimistic concurrency. Ignored on create. |
Upload handshake (uploadDocument)
uploadDocument returns uploadUrl and expiresAt. PUT the raw bytes to uploadUrl
without an Authorization header, set Content-Type to the file's MIME type, then
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, andstoreText(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.
getDocumentTextserves the retained text: always available for text-ingested documents, and for file uploads unless uploaded withstoreText: 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.*
| Method | Purpose |
|---|---|
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)
| Field | Type | Notes |
|---|---|---|
name | string | Required. |
description | string | Optional. |
parentFolderId | string | Applied at create only; ignored on update. Omit to create under the context root. |
slug | string | Stable, sibling-unique slug; derived from the name when omitted. Lowercase letters/digits/hyphens. Immutable. |
userId | string | Ownership (Vectros user UUID). |
scopes | string[] | Ownership entity edges, each <namespace>:<value>, at most two. |
expectedVersion | number | Optimistic 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.
parentFolderIdis 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.
- 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-nullparentFolderId(the root's id). - PATCH patchable keys:
name,description,userId,scopes,expectedVersion.slugandparentFolderIdare immutable and rejected.
Lookups & references
Lookup modes
Exactly one mode per call:
| Mode | Parameters | Constraints |
|---|---|---|
| exact | value (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). |
| range | from + to | Both required. Inclusive, ascending. Non-sensitive, single-field lookups only. |
| prefix | prefix | String, 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.
Unique vs non-unique (enumeration)
- A
uniquelookup 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
- Partner-declared lookup fields 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
| Field | Type | Notes |
|---|---|---|
id | string | Version-row id. |
changeType | enum | CREATE | UPDATE | DELETE. |
previousContent | string | JSON-stringified snapshot of the state prior to this change. Populated on UPDATE/DELETE; null on CREATE. JSON.parse to inspect. |
previousVersion | number | Version number before this change; null on CREATE. |
changeReason | string | Caller-supplied or system-derived reason. |
changedBy | string | Id of the user / key responsible. |
createdAt | string | ISO-8601 timestamp of the row. |
changedFields | object | Field-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:
| Status | Meaning (data-model context) |
|---|---|
400 | Validation 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_CONFLICT | expectedVersion did not match — the entity changed since you read it; it is left untouched. |
501 not_implemented | A reserved-but-unbuilt operation (batch record write/lookup/get). |
Where to go next
- explanation.md — the concepts and the why.
- how-to.md — runnable guides for every operation above.
- ../operations-trust/compliance.md — version history retention, the tamper-evident chain, and sensitive-data handling.