ObjectOS
Reference

Field Types

Every field type you can declare on an object — what it stores, what options it accepts, how it surfaces in REST, the UI, and the AI Builder.

48 built-in field types, grouped by family. The full Zod schema is in packages/spec/src/data/field.zod.ts — this page is a working summary.

Core properties (every field)

PropertyTypeDefaultPurpose
namestring (snake_case)Machine identifier — REST path segment, SQL column
labelstringDisplay label in the UI
typeFieldTypeSee type tables below
requiredbooleanfalseNOT NULL constraint
uniquebooleanfalseUnique index
searchablebooleanfalseIndexed for /api/v1/search
multiplebooleanfalseStore an array of values
defaultValueunknownInitial value (literal or CEL)
hiddenbooleanfalseHide from default views
readonlybooleanfalseDisable in forms
systembooleanfalseAuto-injected (id, created_at, …)
externalIdbooleanfalseEligible for upsert via external key
inlineHelpTextstringTooltip / helper text
conditionalRequiredP predicateRequired when CEL is true
trackHistorybooleanfalseRender value changes as human-readable entries on the record activity timeline (ADR-0052)

Removed in 16.0: columnName (the physical column is always the field name; external/federated objects map columns via external.columnMap) and the field-level index boolean (declare indexes in the object's indexes[] array).

Text family

TypeUse forKey options
textshort stringsmaxLength, minLength
textareamulti-linemaxLength
emailemailformat-validated; lowercased
urlURLformat-validated
phonephoneE.164
passwordone-way secretshashed by the auth subsystem; never returned by GET
secretreversible secrets (API keys, tokens, DB passwords)encrypted at rest via the crypto provider, stored as an opaque ref, masked on read. Fail-closed — with no provider configured, writes throw rather than persist cleartext
markdownmarkdown bodyrendered in preview
htmlsanitised HTMLDOMPurify on write
richtextWYSIWYGrich-text editor + serialised JSON

Numbers

TypeUse forKey options
numberfloatsmin, max, precision, scale
currencymoneycurrencyConfig: { precision, currencyMode: 'fixed' | 'dynamic', defaultCurrency }
percent0–100 %min, max, scale

integer and decimal aren't separate types — use number with scale: 0 for integer, precision+scale for fixed decimal.

Date / time

TypeStoresNotes
datecalendar dateno timezone
datetimeinstantUTC
timewall-clock timeno date

Logic

TypeNotes
booleanCheckbox
toggleSame as boolean, switch UI

Selection

TypeNotes
selectSingle choice — options declared inline or referenced from a picklist
multiselectMany choices, stored as an array
radioUI alias for select with radio rendering
checkboxesUI alias for multiselect with checkbox rendering

Options shape:

options: [
  { value: 'low',    label: 'Low' },
  { value: 'high',   label: 'High',   color: '#e02' },
  { value: 'urgent', label: 'Urgent', color: '#c00' }
]

Relations

TypeCardinalitySemantics
lookupmany-to-oneLoose reference; deleting the parent doesn't delete the child by default
master_detailmany-to-one, cascadingChild cannot exist without parent; permissions inherit from parent
treeself-referenceHierarchical (parent_id on the same object)

Common options:

{
  type: 'lookup',
  reference: 'account',                       // target object name
  lookupFilters: [                            // narrow the lookup picker
    { field: 'status', operator: 'eq', value: 'active' }
  ],
  deleteBehavior: 'set_null'                  // 'set_null' | 'cascade' | 'restrict'
}

The old string-array referenceFilters was removed in 16.0 — the record picker only honours the structured lookupFilters form. Supported operator values: eq, ne, gt, lt, gte, lte, contains, in, notIn.

Computed

TypeWhat it doesKey option
formulaDerived value, evaluated on read or recalcexpression: F\record.qty * record.unit_price`` — see CEL
summaryRoll-up over a child relationsummaryOperations: { object, field, function, filter? } (count | sum | avg | min | max; optional filter — a query-where condition, e.g. { status: 'received' } — aggregates only matching child rows, 16.0+)
autonumberAuto-incrementing display numberformat (e.g. TKT-{0000}), startAt

Formula example:

{
  name: 'profit_margin',
  type: 'formula',
  expression: F`(record.revenue - record.cost) / record.revenue * 100`
}

Media

TypeUse forKey option
imageimage attachmentsmultiple / accept / maxSize (see below)
fileany filesame
avatarprofile picturessquare crop, sensible defaults
videovideo uploadsduration + thumbnail capture
audioaudiowaveform preview
{
  type: 'file',
  multiple: true,                             // store an array of attachments
  accept: ['image/png', 'image/jpeg'],        // MIME types / extensions
  maxSize: 10_000_000                         // bytes
}

What a media field stores

A media field stores an opaque sys_file id — not a URL, and not an inline metadata blob. The { url, name, size, mimeType, … } object you see when you read a record is the expanded (read) form, derived by the platform from the owning sys_file record; url comes from the /files/:fileId resolver rather than from anything stored on your row.

This is what makes the constraints real. Because the platform owns the bytes, accept and maxSize are not just hints handed to the browser's file picker — the server re-checks a record write against them authoritatively, so a caller that talks to the API directly cannot bypass them. It is also what lets a download carry the real filename and content type, and read authorization be delegated to the owning object instead of handed out as an unguessable URL.

Existing deployments convert with os migrate files-to-references. See Storage for where the bytes actually live and the 17.0 release notes for the migration's self-check and per-deployment flag.

The nested fileAttachmentConfig object was removed in 16.0 — it was never read by the runtime. Use the flat multiple / accept / maxSize properties. Storage is configured per deployment (see Configure → Storage), not per field.

Structured

TypeStoresNotes
jsonarbitrary JSONStored as JSONB on Postgres
compositesub-record with named fieldsInline struct, not a separate table
repeaterarray of composite valuesOne-to-many without a child object

Enhanced UI

TypeNotes
locationlat/long + accuracy
addressstreet / city / region / postal / country
codesource-code field — language, theme, lineNumbers
colorcolorFormat: 'hex' | 'rgb' | 'rgba' | 'hsl', presetColors[]
rating1–N stars — max, icon
sliderbounded number with slider UI
signaturedrawn signature, stored as image
qrcoderenders the value as a QR or barcode (EAN / UPC / Code128)
progressderived percent rendered as a bar
tagsfree-form tag array with autocomplete
vectorembedding column — flat dimensions (e.g. 1536 for OpenAI embeddings). The nested vectorConfig (distanceMetric/indexed/indexType) was removed in 16.0 — it was never read

System fields (auto-injected on every object)

FieldTypeNotes
idtext (ULID)primary key
created_atdatetimeUTC insert time
updated_atdatetimeUTC last-write time
created_bylookup → userwho inserted
updated_bylookup → userwho last wrote
versionintegeroptimistic-concurrency token

You don't declare these — opt out per object with ObjectSpec.systemFields: false (rarely a good idea).

How fields flow through the stack

*.object.ts (field spec)

   ├─► Postgres / MySQL / SQLite column + index + constraint
   ├─► REST: validated on POST/PATCH, exposed on GET
   ├─► UI: form widget + list column
   ├─► AI Builder: tool argument schema (so the AI knows what to ask)
   └─► Audit: change-tracked if `trackHistory: true`

See also

On this page