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
namestringColumn name. Required.
descriptionstringCarried to schema exports and introspection.
typestringstringLogical type. An unrecognised value is a custom-type or embeddable reference, not an error.
defaultvaluescalar / expression / mapValue applied on create when the caller sent nothing.
nullableboolfalsefalse emits NOT NULL.
requiredboolfalseRuntime presence check on create. No DDL effect.
uniqueboolfalseEmits a column-level UNIQUE.
modifierslistWrite-path gates: final, writeonce, readonly, writeonly (plus required / unique).
validationslistOrdered runtime rules.
transformslistOrdered value-mutation pipeline.
complianceslistData-protection directives. compliance: (singular) is appended to the same list.
computedbool / string / mapMarks the field computed and carries the legacy expression.
compute-strategystringmaterializedvirtual or materialized.
compute-runtimestringexprLegacy language token for the computed expression.
scriptstringLegacy expression body. Setting it alone marks the field computed.
computeobjectNested compute block: script keys plus triggers:.
enumstringReference to a named entry under top-level enums:.
enumvalueslist of stringInline value list, no metadata.
maxlengthintShorthand for a maxlength validation. Does not size the column.
minlengthintShorthand for a minlength validation.
stateflowstringForces the field’s type to state. The name itself is discarded.
state-transitionslist of {from, to}Kept on the field; no runtime reader.
referencesobjectInline foreign key: {entity, field, onDelete, onUpdate}.
embeddablestringNames a top-level embeddables: entry.
storage.codecstringClickHouse per-column codec, e.g. ZSTD(3).
storage.compressionstringKept on the field; no dialect reads it.
storage.encodingstringKept on the field; no dialect reads it.
positionintdeclaration orderColumn-order override when greater than 0.
deprecatedboolfalseKept on the field; no exporter reads it.
deprecated-messagestringKept alongside the flag; no reader.
tokenizedboolfalseAppends a tokenize compliance.
tokenstrategystringAlgorithm name used by that synthesised compliance.
metadatamapFree-form map; other blocks read their own keys from it.
attributesmapSecond 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>)

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.

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 onlyREQUIRED
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.

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. The expression is evaluated by the script runtime — it is not pushed into SQL, so SQL syntax in a computed expression does not evaluate.

compute-strategyEvaluatedPersisted
virtualon read, after rows are scannedno
materializedon create and update, before the statement runsyes, into a real column

Any token other than virtual — including a typo such as virtaul — resolves to materialized, and so does omitting the key on a field declared computed through computed: or script:. The one exception: a field whose only compute declaration is a nested compute: block with neither compute-strategy: nor triggers: is virtual.

fields:
- name: takehome
type: int
computed: "basic + bonus - (professionaltax + incometax)"
compute-strategy: materialized
- name: display_name
type: string
nullable: true
compute:
language: expr
expression: "first_name + ' ' + last_name"
compute-strategy: virtual

Bindings available to the expression are row (the row or payload as a map), entity (the entity name), tenant and user_id. The materialized path additionally promotes every payload key to the top level, so qty * unit_price resolves directly.

language: accepts expr (the default), cel, js, javascript, lua, starlark, go and wasm. The body goes in expression: for a one-liner or script: for a full program; function: names the entry point for the module languages.

The data.* service namespace — data.query, data.count, data.aggregate and the rest — is injected only for js / javascript, lua, starlark and go. An expr, cel or wasm body cannot reach it.

Two further limits are worth knowing before you rely on computed values:

  • The engine never emits GENERATED ALWAYS AS … STORED. Derived values are the engine’s responsibility, so anything writing to the table with direct SQL bypasses computation.
  • An expression that fails to evaluate does not fail the request. The error is recorded in the response metadata under computed:error:<field> or materialized:error:<field> and the field is left as-is.
  • script-file: is not wired. Any script reference with a file and no inline body errors at evaluation time.

A materialized field can declare the source entities whose mutations invalidate it. Same entity computation needs no triggers.

KeyMeaning
entitySource entity. Required.
oncreate, update, delete. Empty matches all three.
affected_viaSingle source column selecting the target row.
affected_via_colsComposite key columns into the target row.
affected_via_exprScript expression deriving the target key.
when_columnsUpdate-only: skip the refresh when none of these columns changed.
whenScript predicate gate. Empty always enqueues.

Exactly one of affected_via, affected_via_cols and affected_via_expr may be set; more than one is a load error, as is a trigger naming an unknown entity or sitting on a field that is not materialized.

fields:
- name: order_count
type: int
nullable: true
computed: true
compute-strategy: materialized
compute:
language: javascript
script: |
return data.count("orders", { customer_id: row.id, status: "completed" });
triggers:
- entity: orders
on: [create, update, delete]
affected_via: customer_id
when_columns: [customer_id, status]
when:
language: expr
expression: "row.status == 'completed'"

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:, algorithm: and category:.

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 classificationcategory:

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: points at a named entry under top-level enums:; enumvalues: declares the values inline without metadata. Both are read by schema exporters. Neither constrains writes: enum and state fields land as plain text columns, no enum type is created during migration, and no validator checks membership. Add an explicit enum validation, as above, to enforce the value set on every backend.

stateflow: forces the field’s type to state whatever type: says, and does nothing else — the loader discards the name. A state machine attaches to a column through its own field: key, not through this one. See Enums and 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 v2.create_failed or v2.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.