Skip to content
Talk to our solutions team

Fields

A field is one entry under entities[].fields[]. It becomes one column in the entity’s table, plus a set of runtime rules the engine applies on every write and read.

Every key below is read by the loader the deployable data.svc uses at boot, a field with no name is a hard load error; everything else is optional.

KeyTypeDefaultWhat it does
namestring—Column name. Required.
descriptionstring—Carried to schema exports and introspection.
typestringstringLogical type. An unrecognised value is a custom-type or embeddable reference, not an error.
defaultvaluescalar / expression / map—Value applied on create when the caller sent nothing.
backfillliteral—Value existing rows get when this column is ADDED to a populated table. See Adding a required column.
nullableboolfalsefalse emits NOT NULL.
requiredboolfalseRuntime presence check: on create, and on any update that names the field. No DDL effect.
uniqueboolfalseEmits a column-level UNIQUE.
modifierslist—Write-path gates: final, writeonce, readonly, writeonly (plus required / unique).
validationslist—Ordered runtime rules.
transformslist—Ordered value-mutation pipeline.
complianceslist—Data-protection directives. compliance: (singular) is appended to the same list.
computedbool / string / map—Marks the field computed and carries the legacy expression.
compute-strategystringmaterializedvirtual or materialized.
compute-runtimestringexprLegacy language token for the computed expression.
scriptstring—Legacy expression body. Setting it alone marks the field computed.
computeobject—Nested compute block: script keys, strategy: (the canonical home of the strategy) and triggers:.
enumstring—Names a top-level enum. The field becomes an enum field, stores the enum’s key and references the enum’s table; see enums.
enumvalueslist of string—Inline value list, no metadata.
maxlengthint—Shorthand for a maxlength validation. Does not size the column.
minlengthint—Shorthand for a minlength validation.
stateflowstring—Binds the named schema-level state machine to this field, and forces the field’s type to state.
state-transitionslist of {from, to}—Kept on the field; no runtime reader.
referencesobject—Inline foreign key: {entity, field, onDelete, onUpdate}.
embeddablestring—Names a top-level embeddables: entry.
searchobject—enabled: true puts the field in the entity’s full-text document (it must also be named in the entity’s search.document); trigram: true gives it a trigram index for substring filters. Independent of each other. See Search and vectors.
dimint—type: vector only. Dimension. Required; part of the column type.
metriccosine | l2 | ip—type: vector only. Distance metric. Required; selects the index operator class and the near operator.
precisionfloat32 | float16 | binary | sparsefloat32type: vector only. Storage form: vector, halfvec, bit or sparsevec. There is no int8.
indexobject{kind: hnsw}type: vector only. ANN index: kind (hnsw | ivfflat | none), m and ef_construction for hnsw, lists for ivfflat. none is right below a few thousand rows.
storage.codecstring—ClickHouse per-column codec, e.g. ZSTD(3).
storage.compressionstring—Kept on the field; no dialect reads it.
storage.encodingstring—Kept on the field; no dialect reads it.
positionintdeclaration orderColumn-order override when greater than 0.
deprecatedboolfalseKept on the field; no exporter reads it.
deprecated-messagestring—Kept alongside the flag; no reader.
tokenizedboolfalseAppends a tokenize compliance.
tokenstrategystring—Algorithm name used by that synthesised compliance.
metadatamap—Free-form map; other blocks read their own keys from it.
attributesmap—Second free-form map, kept distinct so both spellings round-trip.

These keys parse and are then discarded. They have no effect: hidden, serial, subfields, annotations, stateflowfield. A second group survives onto the field but nothing in the engine, the DDL emitter or the exporters reads it: state-transitions, deprecated, deprecated-message, storage.compression, storage.encoding.

type: takes a canonical token or one of its aliases. Anything unresolved is carried through as a custom-type or embeddable reference and treated as a string at the runtime layer.

CanonicalAccepted spellings
stringstring, varchar, char
texttext (spelling preserved. This is what widens the column)
intint, integer, int32, int16, int8, uint8, uint16, uint32
bigintbigint, long, int64, uint64
doubledouble, float, float64, float32, real
decimaldecimal, numeric, money
booleanboolean, bool
timestamptimestamp, timestampz, datetime
datedate
timetime
uuiduuid
ulidulid, klid (spelling preserved: 27 characters instead of 26)
jsonbjsonb, json, object
enumenum
statestate
filefile
bytesbytes, bytea, binary
arrayarray, array(<elem>)

A file field keeps a reference in the column, never the bytes. Upload with a multipart create and read back with ?download=true&field=<name>, which streams from the deployment’s file storage. Where the bytes live is a deployment setting, not a schema one: see File fields.

The unsigned spellings, text and klid are kept verbatim on the field so a backend that distinguishes them can emit the exact column type. Per-dialect column types are on Types.

<type>[] is not one of these spellings. The loader does not recognise it, so the field resolves to a custom-type reference typed string; only the DDL emitter strips the [] to produce a native array column, the array-specific engine rules below. No NOT NULL, the default left to the database, key off the logical type and therefore do not apply. Write array(<elem>).

A string field is VARCHAR(255) on PostgreSQL and DuckDB. maxlength: adds a runtime validation and nothing else, so a 400-character value passes validation and then fails at the database. To widen the column, declare a custom type carrying length:, precision: or scale: and reference it from type:, or use type: text for an unbounded column.

type: string(255) is accepted as the inline spelling of that validation: string(n), varchar(n) and char(n) load as a plain string with maxlength: n, and mean exactly what the key means, including that they do not widen the column. A parameterised spelling of any other type, decimal(20,6), fails the load naming what is supported, because nothing downstream reads those parameters.

There is also no field-level index: key. A single-column index comes from unique: true; everything else is declared in the entity’s indexes: block.

fields:
- name: code
type: string
modifiers: [required, unique]
- name: email
type: string
nullable: true
DeclarationEnforced whereOn failure
nullable: false (the default)database, via NOT NULLdriver error, not a validation error
required: trueengine, on create and on an update that names the fieldREQUIRED
unique: truedatabase, via column UNIQUEdriver error
validations: [{type: unique}]database. The loader lifts it to unique: truedriver error

NOT NULL is skipped for array fields and for materialized computed columns. Empty and whitespace-only strings fail the required check. Uniqueness is never a runtime check: the unique validator returns without querying, so the column constraint is the only enforcement there is.

Adding a required column to a table that has rows

Section titled “Adding a required column to a table that has rows”

A required column becomes NOT NULL, and the rows already in the table have no value for it. That cannot be one statement, so the planner refuses it rather than emitting DDL that fails partway:

column “status” is required, so it becomes NOT NULL, and the rows already in this table have no value for it: declare backfill: to fill them once, or defaultvalue: to give every row one, or write the sequence yourself as a migrations[].scripts[] entry.

backfill: is that value:

- name: status
type: string
modifiers: [required]
backfill: pending

With it, the migration becomes four steps in this order:

  1. add the column nullable — the existing rows have no value yet
  2. UPDATE ... WHERE status IS NULL — give them one
  3. SELECT count(*) WHERE status IS NULL — must be 0
  4. SET NOT NULL — tighten, now that it is true

Step 3 is what makes it safe to run unattended: step 4 fails on any row step 2 missed, and a failure there leaves a nullable column rather than a half-changed table. Step 2 is restricted to rows without a value, so a retry touches only what it must.

backfill: takes a literal — a string, number or boolean. It is rendered into the UPDATE with its quotes escaped, which is why it is restricted: a literal is portable across every backend and carries no SQL. Anything computed from other columns is a migrations[].scripts[] entry with kind: dml, where raw SQL is the stated contract.

defaultvalue: is applied on create only, and only when the caller supplied no value for the field. It is never re-applied on update, so clearing a field stays cleared. Array fields are skipped by the engine and rely on the database-side default instead.

ExpressionValueWhere it is resolved
ulid()26-character Crockford ULIDengine; no SQL DEFAULT emitted
uuid(), uuid_v4()UUID v4engine; no SQL DEFAULT emitted
uuid7()RFC 9562 time-ordered UUID v7engine; no SQL DEFAULT emitted
klid('X')X + ULID, 27 charactersengine; no SQL DEFAULT emitted
'X'+ulid()same as klid('X')engine; no SQL DEFAULT emitted
contextget("<slot>")—no SQL DEFAULT emitted, and no engine resolver either
currentTimestamp()—CURRENT_TIMESTAMP in the DDL, but no engine resolver
now(), now, current_timestampcurrent UTC timeengine at insert and CURRENT_TIMESTAMP in the DDL
anything elsethe literal you wroteemitted as a SQL DEFAULT

Matching is case-insensitive and tolerates whitespace, the prefix argument to klid() is mandatory, bare klid() is treated as a literal string, a string that merely looks like a call, such as foo(), is stored as text.

Modifiers gate the write path. They can be written under modifiers: or mixed into validations:. Both lists are split by the same resolver. required and unique written in either list set the boolean flags rather than adding a modifier.

modifiers: [final]
modifiers:
- type: final
message: "cannot change once set"
ModifierAlso acceptedEnforcedError code
finalimmutablerejects the field’s presence in an update payloadFINAL
writeoncewrite-once, writeonceonlyrejects the field’s presence in an update payloadWRITE_ONCE
readonlyread-onlyrejects the field’s presence in an update payload; create is allowedREADONLY
writeonlywrite-onlyread side. The field is stripped from returned rows—

The check only fires when the payload actually carries a value for the field, so an update that omits a final field succeeds. final, writeonce and readonly are the same rule with three error codes; none of them blocks a create.

The audit hook overwrites fields named exactly createdon, updatedon, createdby and updatedby on create, and updatedon and updatedby on update, regardless of what the caller sent and regardless of their modifiers, a field named version is different: it is set to 1 on create only when the caller supplied nothing, and on update the compiler increments it rather than the hook writing a value.

A field is computed when it declares computed:, script:, or a compute: block. It is evaluated by the script runtime, virtual on read, materialized into a real column on write, and a materialized field can declare cross-entity refresh triggers.

The strategies, declaration forms, bindings, languages, triggers and the traps that make a computed field silently wrong are on their own page: Computed fields.

references: on a field declares the foreign key inline. Schema-level relations, with cardinality and named traversals, are covered on Entities.

KeyRequiredNotes
entityyesTarget entity.
fieldyesTarget column.
onDeletenoNote the camelCase spelling.
onUpdatenoSame set as onDelete.

Referential actions parse case-insensitively from CASCADE, SET NULL (or SETNULL) and RESTRICT. Anything else, including omission, is NO ACTION.

- name: customer_id
type: ulid
references:
entity: customer
field: id
onDelete: SET NULL

compliances: classifies the field and declares how it is protected. Each entry takes type: plus, depending on the directive, value:, key: and algorithm:. Any other key, including the older category:, fails the load.

DirectiveEffectParameters
maskMasks the value on readvalue:: the pattern
redactReplaces the value with [REDACTED] on read—
hiddenRemoves the field from read output—
nologKeeps the value out of logs—
noexportKeeps the field out of exports—
noindexSuppresses index emission over the field—
encryptedEncrypts at restkey:, algorithm:
tokenizeStores a token instead of the value—
hashStores a one-way digest—
retentionMarks a retention policyvalue:
searchableOpts the field out of default encryption—
pii, phi, pci, gdprSensitivity classification—

Declaring any of the four classifications applies a default bundle per dimension:

  • mask on read, unless you already declared mask, redact or hidden, a pci field defaults to the last4 pattern.
  • encrypt at rest under key default, unless you already declared encrypted, tokenize or hash, or opted out with searchable.
  • nolog and noexport, unconditionally.
  • noindex whenever encryption or tokenization is in effect.

Encryption keys are held in Vault.

Full rules go in validations:, an ordered list evaluated on create and update. Every entry takes these keys; the rest of the parameters are read straight off the entry according to the rule’s type:.

KeyMeaning
typeWhich rule to run. Required.
valueSingle-value parameter: bound, pattern, substring, or exact length.
messageOverrides the generated failure message.
codeOverrides the machine-readable error code.
min, maxBounds for length, minmax and filesize.

An unrecognised type: is not ignored. It rejects the write with VALIDATION_UNKNOWN. Two keys parse but have no reader: scope: (intended for compound-unique) and when:, so a rule declared conditional runs unconditionally. There is no exists validator; declaring one rejects every write that carries a value for the field.

- name: status
type: string
enum: document_status
defaultvalue: draft
validations:
- type: enum
values: [draft, submitted, approved, cancelled]

enum: names a top-level enum. The field becomes an enum field whatever type: says, its column is plain text holding the enum’s key, and it references the enum’s table (enum_<name>), which the engine enforces on every write, on every backend: an unknown, deleted or deprecated key is 422 reference_missing. No backend gets a database enum type. enumvalues: declares an inline list with no table and no metadata; add an explicit enum validation, as above, when that set must hold.

stateflow: binds the named machine — declared once under the schema’s top-level stateflows: — to this field, and forces the field’s type to state whatever type: says. The binding is what names the column the machine runs on, so one definition can govern a field of several entities. See Stateflows.

Two built-in traits are applied to every entity, and two more are opt-in via inherits:. Fields you declare yourself always win over a trait field of the same name.

TraitFieldTypeShape
common (automatic)createdbystringdefault contextget("user"), final, length 3–255
common (automatic)createdontimestampdefault currentTimestamp(), final
common (automatic)updatedbystringnullable, materialized compute
common (automatic)updatedontimestampnullable, materialized compute
softdelete (automatic)deletedbystringnullable, writeonce
softdelete (automatic)deletedontimestampnullable, writeonce
id (opt-in)iduliddefault ulid(), required, unique, final
uid (opt-in)idkliddefault 'U'+ulid(), required, unique, final

If the entity has no primary-key: and no id field survives trait merging, the loader injects the id column from the id trait, an entity with an explicit natural key gets no such injection, the presence of deletedon is what turns soft delete on: deletes become an update and reads filter on deletedon IS NULL.

Row-history strategies add more columns. type2 and type6 synthesise readonly valid_from, valid_to (both renameable via history.valid_from_field and history.valid_to_field) and _version_num; type3 and type6 add a readonly <field>_prev for every name under history.tracked. See Entities for the strategies themselves.

The entity below is loaded end-to-end against PostgreSQL by the block’s own functional test product. It never declares id, createdon, createdby, updatedon, updatedby, deletedon or deletedby, the traits supply all seven.

entities:
- name: customer
description: "Customer master data"
fields:
- name: code
type: string
modifiers: [required, unique]
- name: name
type: string
modifiers: [required]
- name: email
type: string
nullable: true
- name: credit_limit
type: integer
defaultvalue: 0
- name: tenant_id
type: string
nullable: true
- name: owner_id
type: string
nullable: true
access:
actions:
read: { language: expr, expression: "true" }
create: { language: expr, expression: "true" }
update: { language: expr, expression: "true" }
delete: { language: expr, expression: '"admin" in user.roles' }

Field-level failures are typed, so they surface as 422 responses whose envelope code is create_failed or update_failed, the per-field codes named above, REQUIRED, READONLY, FINAL, WRITE_ONCE, VALIDATION_UNKNOWN and the per-rule codes, stay on the internal error and are not serialised into the body; what the client reads is the first failure’s message, the request and response shapes are at Data API endpoints.