ObjectStackObjectStack

Field types — the complete visual gallery

Complete reference for every ObjectStack field type with per-type configuration properties

ObjectStack provides a comprehensive set of field types covering every data modeling need — from basic text and numbers to AI vectors and rich media. This guide organizes them by category with per-type configuration details.

Source: packages/spec/src/data/field.zod.ts
Import: import { FieldType, FieldSchema } from '@objectstack/spec/data'


Text Types

text

Single-line plain text input.

PropertyTypeDefaultDescription
maxLengthnumber—Maximum character length
minLengthnumber—Minimum character length
formatstring—Display hint, not validation — the server runs no check from it. The UI's cell-renderer resolver reads a small set of words and renders the cell as the richer type: phone / tel / telephone (a tel: link), email (a mailto: link), url / uri / link (a clickable link), currency / money, percent / percentage; any other word renders as plain text. To reject a malformed email, URL or phone number, use that field type instead, or a format validation rule
valueDomain'iana_time_zone' | 'iso_4217_currency' | 'iso_3166_alpha2'—Standard the written value must be a member of (IANA time zone, ISO 4217 currency code, ISO 3166-1 alpha-2 country code); text only
{ name: 'first_name', label: 'First Name', type: 'text', maxLength: 100 }

textarea

Multi-line text input for longer content.

PropertyTypeDefaultDescription
maxLengthnumber—Maximum character length
minLengthnumber—Minimum character length
{ name: 'description', label: 'Description', type: 'textarea', maxLength: 5000 }

email

Email address with built-in format validation.

PropertyTypeDefaultDescription
maxLengthnumber—Maximum character length
{ name: 'email', label: 'Email', type: 'email', required: true, unique: 'organization' }

url

URL with format validation.

PropertyTypeDefaultDescription
maxLengthnumber—Maximum character length
{ name: 'website', label: 'Website', type: 'url' }

phone

Phone number field.

PropertyTypeDefaultDescription
maxLengthnumber—Maximum character length
{ name: 'phone', label: 'Phone', type: 'phone' }

password

Masked password input. On a generic object the value is plaintext at rest but masked to •••••••• on read (ADR-0100) — it is not one-way hashed. One-way hashing is owned by the auth subsystem and applies only to its identity tables, never to an authored password field. For reversible encrypted-at-rest secrets (API keys, tokens, DB passwords), use the secret type instead; for login credentials, model them on the auth user object.

{ name: 'password', label: 'Password', type: 'password', required: true }

secret

Reversible, encrypted-at-rest value (DB password, API key, token). Unlike password (masked on read but plaintext at rest, or one-way hashed inside the auth subsystem), a secret is encrypted on write via the registered crypto provider, stored as an opaque ref on the row, and masked on read. Fail-closed: with no provider configured, writes throw rather than persist cleartext.

{ name: 'api_key', label: 'API Key', type: 'secret' }

Rich Content Types

markdown

Markdown-formatted text with preview support.

PropertyTypeDefaultDescription
maxLengthnumber—Maximum character length
{ name: 'readme', label: 'README', type: 'markdown' }

html

Rich HTML content.

PropertyTypeDefaultDescription
maxLengthnumber—Maximum character length
{ name: 'body', label: 'Body', type: 'html' }

richtext

WYSIWYG rich text editor content.

PropertyTypeDefaultDescription
maxLengthnumber—Maximum character length
{ name: 'content', label: 'Content', type: 'richtext' }

Number Types

number

Numeric value with optional precision.

PropertyTypeDefaultDescription
precisionnumber—Total digits
scalenumber—Decimal places
minnumber—Minimum value
maxnumber—Maximum value
{ name: 'quantity', label: 'Quantity', type: 'number', min: 0, max: 99999 }

currency

Monetary value. The stored value is a bare number in both modes; the currency code lives in currencyConfig, never in the value.

PropertyTypeDefaultDescription
precisionnumber—Total digits
minnumber—Minimum value
maxnumber—Maximum value
currencyConfig.currencyMode'dynamic' | 'fixed''dynamic'Whether the column is pinned to defaultCurrency (fixed) or displays in the tenant default currency, the localization.currency setting (dynamic); the value is a bare number either way
currencyConfig.defaultCurrencystring'CNY'The column's currency code under fixed; not the displayed currency under dynamic
{ name: 'price', label: 'Price', type: 'currency', currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'USD' } }

A currency amount's decimal places are its currency's ISO 4217 minor unit (2 for USD, 0 for JPY, 3 for KWD), not a field setting, and they are declared nowhere. A currency field takes no scale, and currencyConfig has no decimal-places key: publish-time validation refuses scale on the field and currencyConfig.precision (removed in @objectstack/spec 17.5.0 — no renderer or runtime ever read it), each with a prescription to delete the key. The field-level precision is the amount's total digit count — a DECIMAL(18,2) amount declares precision: 18 — and is never compared with the currency.

percent

Percentage value (0–100 or decimal).

PropertyTypeDefaultDescription
precisionnumber—Total digits
scalenumber—Decimal places
minnumber—Minimum value
maxnumber—Maximum value
{ name: 'discount', label: 'Discount', type: 'percent', min: 0, max: 100 }

Date & Time Types

date

Date value (no time component).

{ name: 'birth_date', label: 'Birth Date', type: 'date' }

datetime

Date and time value with timezone support.

{ name: 'created_at', label: 'Created At', type: 'datetime', readonly: true }

time

Time-only value (no date component).

{ name: 'start_time', label: 'Start Time', type: 'time' }

Boolean Types

boolean

Standard true/false field.

{ name: 'is_active', label: 'Active', type: 'boolean', defaultValue: true }

toggle

Toggle switch (functionally identical to boolean, different UI).

{ name: 'notifications_enabled', label: 'Notifications', type: 'toggle' }

Selection Types

select

Single-choice dropdown.

PropertyTypeDefaultDescription
optionsSelectOption[]required (or picklist)List of available options — non-empty. Name a shared list with picklist: 'NAME' instead, never both. With neither, a select (or radio) is refused at parse.

SelectOption properties:

PropertyTypeRequiredDescription
labelstring✅Display label
valuestring✅Machine value (snake_case)
colorstring—Badge color
defaultboolean—Pre-selected option
{
  name: 'status', label: 'Status', type: 'select',
  options: [
    { label: 'Open', value: 'open', color: 'blue', default: true },
    { label: 'In Progress', value: 'in_progress', color: 'yellow' },
    { label: 'Done', value: 'done', color: 'green' }
  ]
}

multiselect

Multi-choice dropdown. Same options property as select.

{
  name: 'tags', label: 'Tags', type: 'multiselect',
  options: [
    { label: 'Bug', value: 'bug' },
    { label: 'Feature', value: 'feature' },
    { label: 'Enhancement', value: 'enhancement' }
  ]
}

radio

Radio button group (single choice). Same options property as select.

{
  name: 'priority', label: 'Priority', type: 'radio',
  options: [
    { label: 'Low', value: 'low' },
    { label: 'Medium', value: 'medium', default: true },
    { label: 'High', value: 'high' }
  ]
}

checkboxes

Checkbox group (multi-choice). Same options property as select.

{
  name: 'features', label: 'Features', type: 'checkboxes',
  options: [
    { label: 'Email', value: 'email' },
    { label: 'SMS', value: 'sms' },
    { label: 'Push', value: 'push' }
  ]
}

Relational Types

lookup

Reference to a record in another object (foreign key).

PropertyTypeDefaultDescription
referencestringrequiredTarget object name (snake_case)
referenceFiltersstring[]—Removed (#2377, ADR-0049) — no longer a recognized field property. FieldSchema is a strict object, so an unknown key is rejected with guidance, not silently stripped (ADR-0078): the error echoes the offending key and prescribes the replacement. Use structured lookupFilters + dependsOn instead; see Relationships
deleteBehavior'restrict' | 'cascade' | 'set_null''set_null'Behavior when referenced record is deleted. On a required lookup set_null is escalated to restrict, since a NOT NULL foreign key cannot be cleared — whether the set_null was defaulted or written out explicitly. On a multiple: true required lookup the escalation is judged per referencing row: only a row the member removal would leave EMPTY is refused. cascade and restrict are the values honored as written. Where set_null does run, a multiple: true lookup loses only the deleted member — the other members are kept, and a set emptied that way is stored as [], never null
{ name: 'company', label: 'Company', type: 'lookup', reference: 'account' }

user

Person picker — a lookup specialized to the built-in sys_user object. Stored identically to lookup (foreign key to sys_user.id; multiple: true stores an array) and resolved through the same $expand machinery. Supports defaultValue: 'current_user' to stamp the acting user's id on insert.

Field.user({ label: 'Assignee' })
Field.user({ label: 'Watchers', multiple: true })
Field.user({ label: 'Owner', defaultValue: 'current_user' })

master_detail

Parent-child relationship (cascading delete by default).

PropertyTypeDefaultDescription
referencestringrequiredTarget (master) object name
referenceFiltersstring[]—Removed (#2377, ADR-0049) — no longer a recognized field property. FieldSchema is a strict object, so an unknown key is rejected with guidance, not silently stripped (ADR-0078): the error echoes the offending key and prescribes the replacement. Use structured lookupFilters + dependsOn instead; see Relationships
deleteBehavior'restrict' | 'cascade''cascade'Behavior when parent is deleted. restrict refuses the parent delete while children exist; cascade (and omitting the key) deletes the children with the parent. An explicit set_null is a parse-time rejection on this type (#9689) — a detail row cannot outlive its master; use a lookup if children must survive the parent
inlineEditboolean | 'grid' | 'form'—Edit child records inline on the parent create/edit form (true = auto-pick, 'grid', or 'form')
inlineColumnsarray—Optional explicit inline grid columns
inlineAmountFieldstring—Numeric child field used for the inline running total
{
  name: 'order',
  label: 'Order',
  type: 'master_detail',
  reference: 'order',
  inlineEdit: true,
  inlineAmountField: 'line_total',
}

tree

Self-referential hierarchy (e.g., categories, org chart).

PropertyTypeDefaultDescription
referencestring—Optional. If given, must be this object (self-reference) — any other object is refused at parse
{ name: 'parent_category', label: 'Parent Category', type: 'tree', reference: 'category' }

Media Types

Media fields store a reference to an uploaded file. They take the universal field properties (multiple is honored for file and image). Two more field properties apply to the media family (file, image, avatar, video and audio) and constrain the referenced file: accept, the permitted upload types as MIME types or extensions (e.g. ["image/*", ".pdf"]), offered to the file picker and enforced on write; and maxSize, the maximum permitted file size in bytes, enforced on write against the stored file size, not just checked in the browser. A create or update whose referenced file violates either is refused with 400 ERR_FILE_CONSTRAINT, and the error message names the field and the constraint. Storage itself is configured at the storage-provider layer, not on the field.

image

Image file with optional validation.

{ name: 'photo', label: 'Photo', type: 'image' }

file

Generic file attachment.

{ name: 'attachment', label: 'Attachment', type: 'file', multiple: true }

avatar

Profile image (typically square, small).

{ name: 'avatar', label: 'Avatar', type: 'avatar' }

video

Video file attachment.

{ name: 'demo_video', label: 'Demo Video', type: 'video' }

audio

Audio file attachment.

{ name: 'recording', label: 'Recording', type: 'audio' }

Computed Types

formula

Calculated field using a CEL expression (see Expressions).

PropertyTypeDefaultDescription
expressionstring | ExpressionrequiredCEL calculation expression
returnType'number' | 'text' | 'boolean' | 'date' | 'currency'—Declared value type of the computed result. currency: an amount of money, in the currency the formula's own currencyConfig declares (none: dynamic)
{ name: 'total', label: 'Total', type: 'formula', expression: 'record.quantity * record.unit_price', returnType: 'number' }

summary

Roll-up summary from child records.

PropertyTypeDefaultDescription
summaryOperations.objectstringrequiredChild object to aggregate
summaryOperations.fieldstringrequiredChild field to aggregate; ignored for count
summaryOperations.functionenumrequiredcount, sum, avg, min, or max
summaryOperations.relationshipFieldstringautoChild FK back to the parent when it cannot be inferred
summaryOperations.filterFilterCondition—Only child rows matching this where predicate are aggregated
{
  name: 'task_count',
  label: 'Task Count',
  type: 'summary',
  summaryOperations: {
    object: 'task',
    field: 'id',
    function: 'count',
    relationshipField: 'project',
  },
}

Add a filter to aggregate only a subset of children — this is how one child object feeds several different totals. The engine ANDs the predicate with the parent-FK match and recomputes on the child's next write, so a row moving in or out of the filter (a status change) keeps the parent current:

// Sum only received receipt lines (3-way match), not every line.
{
  name: 'received_amount',
  type: 'summary',
  summaryOperations: {
    object: 'procurement_receipt',
    field: 'amount',
    function: 'sum',
    filter: { status: 'received' },
  },
}

// One `engagement` child → distinct totals, differentiated only by the filter.
{
  name: 'total_signups',
  type: 'summary',
  summaryOperations: { object: 'engagement', field: 'id', function: 'count', filter: { type: 'signup' } },
}

autonumber

Auto-incrementing number with format template.

PropertyTypeDefaultDescription
autonumberFormatstring—Format template (e.g., 'INV-{0000}')
{ name: 'ticket_number', label: 'Ticket #', type: 'autonumber', autonumberFormat: 'TKT-{0000}' }

An autonumber is unique and monotonic per scope — it is not gapless. Any write that gets rejected after the number was reserved — a unique violation on another field, a failed validation rule, a beforeInsert hook that throws — still consumes that number, and it is never reissued. TKT-0001 succeeding and TKT-0002 failing means the next row is TKT-0003; TKT-0002 will not exist.

This is a decided property of the counter, not a bug to work around. If you are building an invoice, contract, or receipt sequence on an autonumber field, design for gaps: an autonumber guarantees a unique, ever-increasing business identifier, but it is not a legally gapless document number. Treat that guarantee as a separate requirement to satisfy explicitly, not something autonumber already gives you.


Embedded Structured Types

These types store structured values as JSON on the parent row — no separate table or foreign key.

composite

Single embedded sub-object with declared sub-fields (similar to a Strapi component / ACF group).

{ name: 'dimensions', label: 'Dimensions', type: 'composite' }

repeater

Repeating embedded sub-object array with declared sub-fields (similar to a Strapi repeatable component / ACF repeater).

{ name: 'line_items', label: 'Line Items', type: 'repeater' }

record

Name-keyed map of embedded sub-objects (Record<string, SubObject>). Insertion order is display order. Used for collections where each item has a stable machine name. See ADR-0007.

{ name: 'settings', label: 'Settings', type: 'record' }

Enhanced Types

location

Geographic coordinates. Stored as { lat, lng, altitude?, accuracy? } (lat −90..90, lng −180..180). No per-type config properties. The key names are lat/lng, not latitude/longitude — see LocationValueSchema in field-value.zod.ts (ADR-0104 D1). The schema is strict (#13802): an undeclared key is refused by name, not silently dropped.

{ name: 'headquarters', label: 'Location', type: 'location' }

address

Structured postal address. Stored as { street, city, state, postalCode, country, countryCode, formatted } (all parts optional). No per-type config properties. The schema is strict (#13802): an undeclared key such as postal_code or zipCode is refused with a rename to postalCode, not silently dropped.

{ name: 'billing_address', label: 'Billing Address', type: 'address' }

code

Source code with syntax highlighting.

PropertyTypeDefaultDescription
languagestring—Programming language for highlighting
{ name: 'snippet', label: 'Code Snippet', type: 'code', language: 'typescript' }

json

Raw JSON data.

{ name: 'metadata', label: 'Metadata', type: 'json' }

color

Color picker. Stores the color value as a string. No per-type config properties.

{ name: 'brand_color', label: 'Brand Color', type: 'color' }

rating

Star rating input.

PropertyTypeDefaultDescription
maxnumber5Maximum rating value (set by the Field.rating(max) factory)
{ name: 'satisfaction', label: 'Rating', type: 'rating', max: 5 }
// or with the factory:
// satisfaction: Field.rating(5, { label: 'Rating' })

slider

Range slider input.

PropertyTypeDefaultDescription
minnumber—Minimum value
maxnumber—Maximum value
stepnumber1Step increment
{ name: 'confidence', label: 'Confidence', type: 'slider', min: 0, max: 100, step: 5 }

signature

Digital signature capture.

{ name: 'approval_signature', label: 'Signature', type: 'signature' }

qrcode

QR/barcode value rendered as a scannable code. No per-type config properties.

{ name: 'asset_tag', label: 'Asset Tag', type: 'qrcode' }

progress

Progress bar (0–100).

{ name: 'completion', label: 'Completion', type: 'progress' }

tags

Tag input with autocomplete.

{ name: 'labels', label: 'Labels', type: 'tags' }

AI / ML Types

vector

Vector embeddings for semantic search and similarity.

PropertyTypeDefaultDescription
dimensionsnumberrequiredVector dimensionality (e.g., 1536 for OpenAI embeddings)

An earlier nested vectorConfig object was removed — the field builder no longer emits it and it is ignored. Set the flat dimensions property instead.

{ name: 'embedding', label: 'Embedding', type: 'vector', dimensions: 1536 }
// or with the factory:
// embedding: Field.vector(1536, { label: 'Embedding' })

Universal Field Properties

These properties are available on all field types:

PropertyTypeDefaultDescription
namestringrequiredMachine name (snake_case)
labelstring—Human-readable display label
descriptionstring—Field description / help text
typeFieldTyperequiredOne of the supported field types
requiredbooleanfalseWhether the field is required
uniqueboolean | 'global' | 'organization'false ('organization' on autonumber)Unique constraint and its scope (ADR-0120). 'organization' = one holder per organization; true = that same per-organization scope (positional synonym — prefer the explicit spelling in new code); 'global' = one holder across the whole installation; false = no constraint. Omitted ⇒ false, except on an autonumber field, where omitted ⇒ 'organization' — an auto-number is a business identifier, so the platform makes it unique per organization by default; write unique: false explicitly to opt out. 'tenant'/'org' are rejected — the word is 'organization'. Full rule in the unique doc block in FieldSchema
multiplebooleanfalseAllow array of values (multi-record lookup, multi-select, multi-file). An emptied multi-value lookup reads back as [], never null — the multiple doc block in FieldSchema
searchablebooleanfalseInclude in search index
sortablebooleantrueAllow sorting by this field
hiddenbooleanfalseHide from default views
readonlybooleanfalseServer contract — non-system writes to this field are stripped on both INSERT and UPDATE (the value falls back to defaultValue), not just hidden in the UI (#2948 / #3043)
externalIdbooleanfalseExternal system identifier
defaultValueany—Default value for new records
groupstring—Field grouping / section
inlineHelpTextstring—Inline help tooltip
trackHistoryboolean—Render this field's value changes as entries on the record activity timeline
requiredPermissionsstring[]—Capabilities required to read/edit this field (masked on read, denied on write)
dependsOnstring[]—Fields whose values scope this field's options (lookup candidate query; select/radio option gating)
visibleWhenExpression—Show field only when predicate is true
readonlyWhenExpression—Make field read-only when predicate is true
requiredWhenExpression—Require field when predicate is true

Choosing a Field Type

Not sure which type fits your data? Follow the Field Type Decision Tree — a flowchart, quick-reference tables, and a use-case mapping for every type in this gallery.

On this page