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.
Field keys
Section titled “Field keys”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.
| Key | Type | Default | What it does |
|---|---|---|---|
name | string | — | Column name. Required. |
description | string | — | Carried to schema exports and introspection. |
type | string | string | Logical type. An unrecognised value is a custom-type or embeddable reference, not an error. |
defaultvalue | scalar / expression / map | — | Value applied on create when the caller sent nothing. |
backfill | literal | — | Value existing rows get when this column is ADDED to a populated table. See Adding a required column. |
nullable | bool | false | false emits NOT NULL. |
required | bool | false | Runtime presence check: on create, and on any update that names the field. No DDL effect. |
unique | bool | false | Emits a column-level UNIQUE. |
modifiers | list | — | Write-path gates: final, writeonce, readonly, writeonly (plus required / unique). |
validations | list | — | Ordered runtime rules. |
transforms | list | — | Ordered value-mutation pipeline. |
compliances | list | — | Data-protection directives. compliance: (singular) is appended to the same list. |
computed | bool / string / map | — | Marks the field computed and carries the legacy expression. |
compute-strategy | string | materialized | virtual or materialized. |
compute-runtime | string | expr | Legacy language token for the computed expression. |
script | string | — | Legacy expression body. Setting it alone marks the field computed. |
compute | object | — | Nested compute block: script keys, strategy: (the canonical home of the strategy) and triggers:. |
enum | string | — | Names a top-level enum. The field becomes an enum field, stores the enum’s key and references the enum’s table; see enums. |
enumvalues | list of string | — | Inline value list, no metadata. |
maxlength | int | — | Shorthand for a maxlength validation. Does not size the column. |
minlength | int | — | Shorthand for a minlength validation. |
stateflow | string | — | Binds the named schema-level state machine to this field, and forces the field’s type to state. |
state-transitions | list of {from, to} | — | Kept on the field; no runtime reader. |
references | object | — | Inline foreign key: {entity, field, onDelete, onUpdate}. |
embeddable | string | — | Names a top-level embeddables: entry. |
search | object | — | 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. |
dim | int | — | type: vector only. Dimension. Required; part of the column type. |
metric | cosine | l2 | ip | — | type: vector only. Distance metric. Required; selects the index operator class and the near operator. |
precision | float32 | float16 | binary | sparse | float32 | type: vector only. Storage form: vector, halfvec, bit or sparsevec. There is no int8. |
index | object | {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.codec | string | — | ClickHouse per-column codec, e.g. ZSTD(3). |
storage.compression | string | — | Kept on the field; no dialect reads it. |
storage.encoding | string | — | Kept on the field; no dialect reads it. |
position | int | declaration order | Column-order override when greater than 0. |
deprecated | bool | false | Kept on the field; no exporter reads it. |
deprecated-message | string | — | Kept alongside the flag; no reader. |
tokenized | bool | false | Appends a tokenize compliance. |
tokenstrategy | string | — | Algorithm name used by that synthesised compliance. |
metadata | map | — | Free-form map; other blocks read their own keys from it. |
attributes | map | — | 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.
| Canonical | Accepted spellings |
|---|---|
string | string, varchar, char |
text | text (spelling preserved. This is what widens the column) |
int | int, integer, int32, int16, int8, uint8, uint16, uint32 |
bigint | bigint, long, int64, uint64 |
double | double, float, float64, float32, real |
decimal | decimal, numeric, money |
boolean | boolean, bool |
timestamp | timestamp, timestampz, datetime |
date | date |
time | time |
uuid | uuid |
ulid | ulid, klid (spelling preserved: 27 characters instead of 26) |
jsonb | jsonb, json, object |
enum | enum |
state | state |
file | file |
bytes | bytes, bytea, binary |
array | array, 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>).
There is no size: key
Section titled “There is no size: key”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.
Nullability, presence and uniqueness
Section titled “Nullability, presence and uniqueness”fields: - name: code type: string modifiers: [required, unique] - name: email type: string nullable: true| Declaration | Enforced where | On failure |
|---|---|---|
nullable: false (the default) | database, via NOT NULL | driver error, not a validation error |
required: true | engine, on create and on an update that names the field | REQUIRED |
unique: true | database, via column UNIQUE | driver error |
validations: [{type: unique}] | database. The loader lifts it to unique: true | driver 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, ordefaultvalue:to give every row one, or write the sequence yourself as amigrations[].scripts[]entry.
backfill: is that value:
- name: status type: string modifiers: [required] backfill: pendingWith it, the migration becomes four steps in this order:
- add the column nullable — the existing rows have no value yet
UPDATE ... WHERE status IS NULL— give them oneSELECT count(*) WHERE status IS NULL— must be0SET 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.
Default values
Section titled “Default values”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.
| Expression | Value | Where it is resolved |
|---|---|---|
ulid() | 26-character Crockford ULID | engine; no SQL DEFAULT emitted |
uuid(), uuid_v4() | UUID v4 | engine; no SQL DEFAULT emitted |
uuid7() | RFC 9562 time-ordered UUID v7 | engine; no SQL DEFAULT emitted |
klid('X') | X + ULID, 27 characters | engine; 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_timestamp | current UTC time | engine at insert and CURRENT_TIMESTAMP in the DDL |
| anything else | the literal you wrote | emitted 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
Section titled “Modifiers”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"| Modifier | Also accepted | Enforced | Error code |
|---|---|---|---|
final | immutable | rejects the field’s presence in an update payload | FINAL |
writeonce | write-once, writeonceonly | rejects the field’s presence in an update payload | WRITE_ONCE |
readonly | read-only | rejects the field’s presence in an update payload; create is allowed | READONLY |
writeonly | write-only | read 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.
Computed fields
Section titled “Computed fields”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.
Foreign key shorthand
Section titled “Foreign key shorthand”references: on a field declares the foreign key inline. Schema-level relations, with
cardinality and named traversals, are covered on
Entities.
| Key | Required | Notes |
|---|---|---|
entity | yes | Target entity. |
field | yes | Target column. |
onDelete | no | Note the camelCase spelling. |
onUpdate | no | Same 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 NULLCompliance directives
Section titled “Compliance directives”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.
| Directive | Effect | Parameters |
|---|---|---|
mask | Masks the value on read | value:: the pattern |
redact | Replaces the value with [REDACTED] on read | — |
hidden | Removes the field from read output | — |
nolog | Keeps the value out of logs | — |
noexport | Keeps the field out of exports | — |
noindex | Suppresses index emission over the field | — |
encrypted | Encrypts at rest | key:, algorithm: |
tokenize | Stores a token instead of the value | — |
hash | Stores a one-way digest | — |
retention | Marks a retention policy | value: |
searchable | Opts the field out of default encryption | — |
pii, phi, pci, gdpr | Sensitivity classification | — |
Declaring any of the four classifications applies a default bundle per dimension:
- mask on read, unless you already declared
mask,redactorhidden, apcifield defaults to thelast4pattern. - encrypt at rest under key
default, unless you already declaredencrypted,tokenizeorhash, or opted out withsearchable. nologandnoexport, unconditionally.noindexwhenever encryption or tokenization is in effect.
Encryption keys are held in Vault.
Validation entries
Section titled “Validation entries”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:.
| Key | Meaning |
|---|---|
type | Which rule to run. Required. |
value | Single-value parameter: bound, pattern, substring, or exact length. |
message | Overrides the generated failure message. |
code | Overrides the machine-readable error code. |
min, max | Bounds 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.
Enum and state fields
Section titled “Enum and state fields”- 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.
Fields you do not declare
Section titled “Fields you do not declare”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.
| Trait | Field | Type | Shape |
|---|---|---|---|
common (automatic) | createdby | string | default contextget("user"), final, length 3–255 |
common (automatic) | createdon | timestamp | default currentTimestamp(), final |
common (automatic) | updatedby | string | nullable, materialized compute |
common (automatic) | updatedon | timestamp | nullable, materialized compute |
softdelete (automatic) | deletedby | string | nullable, writeonce |
softdelete (automatic) | deletedon | timestamp | nullable, writeonce |
id (opt-in) | id | ulid | default ulid(), required, unique, final |
uid (opt-in) | id | klid | default '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.
Worked example
Section titled “Worked example”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.