Skip to content
Talk to our solutions team

Enums

An enum is a named list of allowed values, each with the text shown for it and optional display metadata, declared once at schema level and referenced from any number of fields. In the database an enum is a table, the same on every backend.

Every enum becomes an ordinary entity named enum_<name>, enum_order_status for the enum order_status (with a datastore prefix of em1_ the table is em1_enum_order_status): a table with a key column and a value column, plus the display metadata, with the audit and soft-delete columns every entity carries. The prefix is on the entity as well as the table, so the path, the schema listing, the reference and a sorted table listing all say what it is. A field that names the enum stores the key and carries a reference to the enum’s table, which the engine enforces on every write, on every backend, exactly as it enforces a declared reference. No backend gets a native enum type.

The enum’s table
keyThe stored value, the primary key. A permanent identity: final, so no write re-keys a row; never empty
valueThe text shown for the key, never empty. It may change; the rows that store the key do not
description, color, icon, groupDisplay metadata, nullable
positionDeclaration order; the table’s default sort
deprecatedStill stored by old rows, refused for new ones

The rows come from the declaration: at bootstrap and after every applied plan the values are upserted by key, and skipped while the declaration has not changed. Change a value’s text in the YAML and the next plan updates the row; nothing that stores the key is touched, so a relabel is never a data migration. To retire a key, mark it deprecated: true: rows that already store it keep working, a new write that names it is refused like an unknown key. Deleting the row is refused while any row still stores the key (409, reference_restrict). To rename a key, add the new key and retire the old one.

CapabilityStatus
Declaration, multi-file merge, name resolutionShips
A table per enum, seeded from the declaration, readable and writable through every routeShips
Membership enforced on write through the reference, on every backendShips (422, reference_missing)
The value’s text through ?include=Ships
Enum names listed by GET /schema; values rendered into the served exports (JSON Schema, OpenAPI, Zod, CUE)Ships
Protobuf, Avro and OData renderersImplemented, no route registered
A database enum type (CREATE TYPE … AS ENUM)Never, on any backend, by design
GraphQL enum types in the generated SDLNot generated: enum fields render as String; the enum’s table is a GraphQL type like any entity

Enums are a top-level list. They are not declared inside an entity.

KeyTypeRequiredMeaning
enums[].namestringYesWhat a field’s enum: key resolves against; the entity and table are enum_<name>. Exact, case-sensitive. A declared entity of that name is a load error
enums[].descriptionstringNoCarried onto the entity
enums[].values[]scalar or mapYesSee the three forms below
enums[].values[].keystringThe stored key
enums[].values[].valuestringThe text shown for the key (with key:); the key itself in the older form
enums[].values[].labelstringThe text shown, in the older form
enums[].values[].description, color, icon, groupstringNoDisplay metadata, columns of the table
enums[].values[].deprecatedboolNoRetires the key for new writes
enums[].accessmapNoThe entity’s access rules. Without it the table is read-only through the API: everyone may read, nobody may create, update or delete, and the declaration is the only writer. Declare the block to let, say, an admin add and retire values

A value takes any of three forms:

enums:
- name: order_status
description: Lifecycle of a sales order.
values:
- draft # the key, shown as itself
- { key: confirmed, value: Confirmed, group: open, color: "#2563eb" }
- { value: shipped, label: Shipped } # the older form: value is the key
- { key: cancelled, value: Cancelled, deprecated: true }

A key declared twice, a value with no key, an unknown key in the mapping form, and an enum that collides with an entity name all fail the load.

KeyTypeEffect
enumstringNames a top-level enum. The field becomes an enum field whatever its type: says, and references the enum’s key
enumvalues[]stringInline value list with no table and no metadata; add a validation to enforce it
entities:
- name: sales_order
fields:
- name: status
type: enum
enum: order_status
defaultvalue: draft

The reference is named <entity>_<field>, here sales_order_status, to enum_order_status.key, many to one, restrict on delete, enforced by the engine and not by a database constraint. A reference the schema declares itself on that column is left as declared. An enum: naming an enum no file declares fails the load.

The enum is a table; the field is a plain text column holding the key:

CREATE TABLE "enum_order_status" ("key" TEXT NOT NULL, "value" TEXT NOT NULL, "description" TEXT, … PRIMARY KEY ("key"));
-- on sales_order
"status" TEXT NOT NULL DEFAULT 'draft'
BackendEnum tableColumn type for enum / state
PostgreSQLa tableTEXT
ClickHousea tableString
DuckDBa tableVARCHAR(255)
SQLitea tableTEXT

Nothing differs between backends: no CREATE TYPE, no check constraint, no foreign key unless the reference asks for one with create: true.

The enum’s table is an entity, so every route serves it. Reads are open; writes need the enum’s own access: block, since the default is read-only:

GET /data/rest/enum_order_status?orderby=position
POST /data/rest/enum_order_status {"key": "on_hold", "value": "On hold"}
PATCH /data/rest/enum_order_status/id/on_hold {"deprecated": true}
enums:
- name: order_status
values: [draft, confirmed, shipped]
access:
actions:
read: { language: expr, expression: "true" }
create: { language: expr, expression: '"admin" in user.roles' }
update: { language: expr, expression: '"admin" in user.roles' }
delete: { language: expr, expression: '"admin" in user.roles' }

With such a block a tenant that needs a value the product did not declare adds a row; the next re-seed leaves it alone, since seeding upserts the declared keys only. A write that changes a row’s key is refused (422, the key is final). The value’s text rides along on a read of the referencing entity through the reference:

GET /data/rest/sales_order?include=sales_order_status
{"data":[{"id":"…","status":"confirmed","sales_order_status":{"key":"confirmed","value":"Confirmed","group":"open"}}]}

Membership is the reference’s job. A write that stores a key the enum’s table does not hold, holds only as a soft-deleted row, or holds as a deprecated one, fails with 422 and the validation code reference_missing, like any reference to a missing parent. The validators still exist for an inline enumvalues: list or an extra constraint:

validations[].typeBehaviour
enumFails when the value is not in values
inIdentical to enum
notinFails when the value is in values
- name: channel
type: enum
enumvalues: [web, pos, partner]
validations:
- type: enum
values: [web, pos, partner]

They run on create and on update, are skipped when the payload does not carry the field, compare with a stringified fallback, and pass silently when values: is omitted.

Enum names are canonicalised to PascalCase wherever a named type is emitted: order_status becomes OrderStatus. The values rendered are the keys.

ExportNamed enum renders as
GET /schema/jsonschema.json$defs.OrderStatus = {"type":"string","enum":[…]}; the field emits {"$ref":"#/$defs/OrderStatus"}
GET /schema/openapi.jsoncomponents.schemas.OrderStatus; the field emits a $ref
GET /schema/zod.tsexport const OrderStatusEnum = z.enum([…]) plus an inferred type
GET /schema/cue#OrderStatus: "draft" | "shipped"
GET /schema/avro.avsca named enum type with the keys as symbols; the field references it by name
GET /schema/protoenum OrderStatus { ORDER_STATUS_UNSPECIFIED = 0; … }
GET /schema/odata.xmlan EnumType with one Member per key
GET /schema/asyncapi.jsonthe event payloads carry the same $defs the JSON Schema export does
GET /schema/graphqlString. No GraphQL enum type is generated
Protobuf (no route)enum OrderStatus with an ORDER_STATUS_UNSPECIFIED = 0 member prepended and values upper-snake-cased
Avro (no route)An enum record whose symbols are the values with every character outside [A-Za-z0-9_] replaced by _
OData (no route)<EnumType Name="OrderStatus"> with ordinal Value attributes

No renderer carries label, color, icon, group, description or deprecated. Exports contain values only.

GET /schema returns the enum names for the tenant as a flat list. The per-entity detail response does not report which enum a field references; use the exports for that.

Enums merge by name across every YAML file under the loaded root, last write wins: a later file declaring order_status replaces the earlier one wholesale, with no diagnostic. This is the opposite of entities and traits, which keep the first definition and log the duplicate. There is no deep merge: you cannot add a value to an enum by re-declaring it with one entry.