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. |
nullable | bool | false | false emits NOT NULL. |
required | bool | false | Runtime presence check on create. 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 plus triggers:. |
enum | string | — | Reference to a named entry under top-level 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 | — | Forces the field’s type to state. The name itself is discarded. |
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. |
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>) |
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.
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 only | 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.
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:, algorithm: and category:.
| 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 | category: |
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: 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.
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
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.