Quick reference for choosing the right field type from 49 available options.
Config columns list only real
FieldSchemakeys. Per-type display knobs beyond these do not exist — an unknown field key is silently stripped at parse (dead metadata), so don't invent config liketheme,rows, orfileAttachmentConfig. Source of truth:node_modules/@objectstack/spec/src/data/field.zod.ts.
| Type | When to Use | Config |
|---|---|---|
text |
Single-line strings (names, codes, titles) | maxLength, minLength, defaultValue |
textarea |
Multi-line plain text (notes, descriptions) | maxLength, minLength |
email |
Email addresses — built-in format validation | required, unique |
url |
Web URLs — built-in format validation | required |
phone |
Phone numbers | format |
password |
secret for credentials; a generic password field triggers a build warning |
minLength, maxLength |
secret |
Reversible encrypted-at-rest credential (DB password, API key, token) — encrypted on write via ICryptoProvider, masked on read, fail-closed (ADR-0100). The recommended type for credentials |
— |
markdown |
Markdown-formatted content | maxLength |
html |
Raw HTML content | maxLength |
richtext |
WYSIWYG rich text editor | maxLength |
| Type | When to Use | Config |
|---|---|---|
number |
Generic numeric value | min, max, precision, scale |
currency |
Monetary amounts | currencyConfig (precision, currencyMode, defaultCurrency) |
percent |
Percentage values (0-100) | min, max, precision |
| Type | When to Use | Config |
|---|---|---|
date |
Date only (no time component) | defaultValue |
datetime |
Full date + time | defaultValue |
time |
Time only (no date component) | defaultValue, format |
| Type | When to Use | Config |
|---|---|---|
boolean |
Standard checkbox | defaultValue |
toggle |
Toggle switch (distinct UI from checkbox) | defaultValue |
| Type | When to Use | Config |
|---|---|---|
select |
Single-choice dropdown | options (value, label, color, default) |
multiselect |
Tag-style multi-choice | options, max |
radio |
Radio button group (fewer choices, always visible) | options |
checkboxes |
Checkbox group | options |
Critical: Every option must have lowercase value and human-readable label.
options: [
{ label: 'In Progress', value: 'in_progress', color: '#3498db' },
{ label: 'Done', value: 'done', default: true },
]| Type | When to Use | Key Config |
|---|---|---|
lookup |
Reference another object (independent) | reference, lookupFilters, multiple |
master_detail |
Parent–child with lifecycle control | reference, deleteBehavior (cascade/restrict/set_null) |
tree |
Hierarchical self-reference | reference |
user |
Person picker — a lookup specialized to sys_user (assignee, watchers). Stored identically to lookup |
multiple (collaborators), defaultValue: 'current_user' |
multiple: truelookup ≠ junction object. A multi-value lookup is stored and read as an array of ids on the record — it is NOT a junction table. Reach for a junction object (two lookups) only when the relationship itself carries attributes (position, added_at, …). (#1872)
| Type | When to Use | Config |
|---|---|---|
image |
Image files (PNG, JPG, GIF, WebP) | multiple |
file |
Generic file attachments | multiple |
avatar |
User/profile picture | — |
video |
Video files | — |
audio |
Audio files | — |
There is no per-field attachment config (size limits, allowed types, storage) — storage concerns live outside the field schema.
Stored as JSON on the parent row — no separate table / FK:
| Type | When to Use |
|---|---|
composite |
Single embedded sub-object with declared sub-fields |
repeater |
Repeating embedded sub-object array |
record |
Name-keyed map of embedded sub-objects (insertion order = display order; ADR-0007) |
| Type | When to Use | Config |
|---|---|---|
formula |
Computed from an expression referencing other fields | expression (CEL, record. prefixes), returnType ('number' | 'text' | 'boolean' | 'date') |
summary |
Roll-up aggregation from child records | summaryOperations ({ object, field, function, relationshipField?, filter? }) |
autonumber |
Auto-incrementing display format ({0000} counter + optional date / {field} tokens, resets per scope) | format (shorthand) / autonumberFormat (canonical) — e.g., "CASE-{0000}", "AD{YYYYMMDD}{0000}" |
| Type | When to Use | Config |
|---|---|---|
location |
Geographic coordinates (lat/lng) | — |
address |
Structured address (street, city, country) | — |
code |
Syntax-highlighted code editor | language |
json |
JSON data (untyped escape hatch) | — (validate with a json_schema validation rule on the object) |
color |
Color picker | — |
rating |
Star/heart rating | max (default 5) |
slider |
Numeric slider | min, max, step |
signature |
Digital signature pad | — |
qrcode |
QR code / barcode | — |
progress |
Progress bar | min, max |
tags |
Free-form tag input | — |
vector |
AI/ML embeddings (semantic search, RAG) | dimensions (flat sibling, e.g. 1536) |
What kind of data?
│
├── Text?
│ ├── Single line → text
│ ├── Multiple lines → textarea
│ ├── Formatted → richtext / markdown / html
│ ├── Email → email
│ ├── URL → url
│ ├── Phone → phone
│ ├── Credential (API key, token, DB password) → secret (encrypted at rest — NOT password)
│ └── Code → code
│
├── Number?
│ ├── Money → currency
│ ├── Percentage → percent
│ └── Generic → number
│
├── Date/Time?
│ ├── Date only → date
│ ├── Time only → time
│ └── Date + Time → datetime
│
├── True/False?
│ ├── Checkbox → boolean
│ └── Switch → toggle
│
├── Choose from list?
│ ├── Single choice, dropdown → select
│ ├── Single choice, always visible → radio
│ ├── Multiple choice, tags → multiselect
│ └── Multiple choice, checkboxes → checkboxes
│
├── Reference another object?
│ ├── Independent → lookup
│ ├── Owned child → master_detail
│ ├── A person (assignee, watcher) → user
│ └── Hierarchy → tree
│
├── File/Media?
│ ├── Image → image
│ ├── Video → video
│ ├── Audio → audio
│ ├── User photo → avatar
│ └── Generic file → file
│
├── Calculated?
│ ├── Formula → formula
│ ├── Roll-up → summary
│ └── Auto-number → autonumber
│
├── Embedded sub-object (no separate table)?
│ ├── Single → composite
│ ├── Array → repeater
│ └── Name-keyed map → record
│
└── Special?
├── Location → location
├── Address → address
├── Color → color
├── Rating → rating
├── Signature → signature
├── QR code → qrcode
├── Progress → progress
├── Tags → tags
├── JSON data → json
└── AI embeddings → vector
{
type: 'text',
maxLength: 255,
required: true,
}{
type: 'email',
required: true,
unique: true,
}{
type: 'currency',
currencyConfig: {
precision: 2,
currencyMode: 'fixed', // 'fixed' = one currency for the column;
// 'dynamic' = per-record `{ value, currency }`
defaultCurrency: 'USD', // ISO 4217
},
}Currency resolution (ADR-0053). A displayed amount resolves its symbol
through: the field's own currencyConfig.defaultCurrency → the tenant
localization.currency default. With neither set, renderers show a plain
grouped number (never a hardcoded $). The same chain backs analytics measures
(a measure's explicit currency wins over the field/tenant default).
{
type: 'select',
required: true,
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium', default: true },
{ label: 'High', value: 'high', color: '#e74c3c' },
],
}{
type: 'lookup',
reference: 'account',
required: true,
// Structured, picker-honoured filter — the former string[] `referenceFilters`
// was removed (#2377, ADR-0049): it filtered nothing.
lookupFilters: [
{ field: 'status', operator: 'eq', value: 'active' },
],
}{
type: 'lookup',
reference: 'tag',
multiple: true,
max: 10,
}{
type: 'master_detail',
reference: 'invoice',
deleteBehavior: 'cascade',
required: true,
}import { F } from '@objectstack/spec';
{
type: 'formula',
expression: F`record.amount * record.tax_rate`, // CEL — `record.` prefixes required
returnType: 'number', // 'number' | 'text' | 'boolean' | 'date' (no 'currency')
}{
type: 'summary',
summaryOperations: {
object: 'invoice_line_item', // child object to aggregate
field: 'amount', // child field (ignored for count)
function: 'sum', // 'count' | 'sum' | 'min' | 'max' | 'avg'
// relationshipField / filter — optional (see rules/relationships.md)
},
}{
type: 'autonumber',
format: 'CASE-{0000}',
}The format is literal text interleaved with {...} tokens:
| Token | Renders | Example |
|---|---|---|
{0000} |
The counter, zero-padded to that many digits (minimum width). At most ONE slot. | CASE-{0000} → CASE-0042 |
{YYYY} {YY} {MM} {DD} {YYYYMMDD} |
Generation date in the request's business timezone | AD{YYYYMMDD}{0000} → AD202606170001 |
{field_name} |
The value of another field on the same record | {plan_no}{000} → PLAN-001001 |
The counter resets per "scope" — everything rendered before the {0000} slot. So AD{YYYYMMDD}{0000} restarts each day, {section}{island_zone}{000} counts per group, {plan_no}{000} counts per parent — no separate reset config. A fixed-prefix format (CASE-{0000}) has an empty scope → one global counter.
Rules — get these wrong and records mis-number silently or fail to save:
- Every
{field}you interpolate must berequired: trueand set before the record is created. An empty interpolated field makes the record number generation throw (the compile lint flags a non-existent field as an error, an optional one as a warning). - Put a delimiter between adjacent variable tokens —
{section}-{zone}{000}, not{section}{zone}{000}. Without one,('AB','C')and('A','BC')both render prefixABCand share a counter (to keep numbers unique). The literal separator keeps distinct groups apart. - Pad width is a MINIMUM, not a cap.
{000}→001…999, then1000(it grows, never wraps). Size it for readability, not as a ceiling. - Only known tokens are interpolated. Date tokens are case-sensitive and exact (
{YYYY}, not{yyyy}or{YYYY-MM}). An unrecognized{...}is emitted literally into the number —{ YYYY }(spaces) renders the text{ YYYY }.
{
type: 'vector',
dimensions: 1536, // flat sibling — OpenAI ada-002
}There is no vectorConfig block — an authored vectorConfig is silently
stripped (dead metadata). dimensions is the flat field-level key.
{
type: 'text', // ❌ No built-in email validation
maxLength: 255,
}{
type: 'email', // ✅ Built-in validation + UI affordances
}options: [
{ label: 'Done', value: 'Done' }, // ❌ Uppercase
]options: [
{ label: 'Done', value: 'done' }, // ✅ Lowercase
]{
type: 'lookup', // ❌ No reference specified
}{
type: 'lookup',
reference: 'account', // ✅ Target object specified
}{
plan_no: { type: 'text' }, // ❌ not required — empty value throws at create
order_no: { type: 'autonumber', format: '{section}{plan_no}{000}' }, // ❌ no delimiter
}{
plan_no: { type: 'text', required: true }, // ✅ always set before generation
order_no: { type: 'autonumber', format: '{section}-{plan_no}-{000}' }, // ✅ delimited
}