| title |
Field Metadata |
| description |
Configure field types and properties — text, numbers, dates, relationships, files, and more |
A Field defines an individual property within an Object. ObjectStack provides a comprehensive set of field types covering text, numbers, dates, selections, relationships, files, calculations, and specialized types like vectors and QR codes.
Fields are defined inside an Object's fields map using Field.* factory methods:
import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Contact = ObjectSchema.create({
name: 'contact',
label: 'Contact',
fields: {
first_name: Field.text({ label: 'First Name', required: true }),
last_name: Field.text({ label: 'Last Name', required: true }),
email: Field.email({ label: 'Email', unique: true }),
phone: Field.phone({ label: 'Phone' }),
birth_date: Field.date({ label: 'Date of Birth' }),
is_active: Field.boolean({ label: 'Active', defaultValue: true }),
account: Field.lookup('account', { label: 'Account', required: true }),
},
});
| Type |
Factory |
Description |
text |
Field.text() |
Single-line text |
textarea |
Field.textarea() |
Multi-line text |
email |
Field.email() |
Email address with validation |
url |
Field.url() |
URL with validation |
phone |
Field.phone() |
Phone number |
password |
Field.password() |
Masked password input |
markdown |
Field.markdown() |
Markdown editor |
html |
Field.html() |
HTML content |
richtext |
Field.richtext() |
Rich text (WYSIWYG) editor |
name: Field.text({ label: 'Name', required: true, maxLength: 255 }),
bio: Field.textarea({ label: 'Bio', maxLength: 5000 }),
website: Field.url({ label: 'Website' }),
notes: Field.markdown({ label: 'Notes' }),
| Type |
Factory |
Description |
number |
Field.number() |
Integer or decimal |
currency |
Field.currency() |
Monetary value with currency support |
percent |
Field.percent() |
Percentage value |
quantity: Field.number({ label: 'Quantity', min: 0, max: 10000, step: 1 }),
price: Field.currency({ label: 'Price', scale: 2, min: 0 }),
// percent stores a fraction: 0.15 = 15% (the UI renders it as a percentage)
discount: Field.percent({ label: 'Discount', scale: 2, min: 0, max: 1 }),
Currency Configuration:
price: Field.currency({
label: 'Price',
currencyConfig: {
precision: 2,
currencyMode: 'dynamic', // 'dynamic' | 'fixed'
defaultCurrency: 'USD',
},
}),
| Type |
Factory |
Description |
date |
Field.date() |
Date only |
datetime |
Field.datetime() |
Date and time |
time |
— |
Time only |
start_date: Field.date({ label: 'Start Date', required: true }),
created_at: Field.datetime({ label: 'Created At', readonly: true }),
is_active: Field.boolean({ label: 'Active', defaultValue: true }),
| Type |
Factory |
Description |
select |
Field.select() |
Single or multi-select dropdown |
radio |
— |
Radio button group |
checkboxes |
— |
Checkbox group |
// Single select
status: Field.select({
label: 'Status',
options: [
{ label: 'New', value: 'new', color: '#999', default: true },
{ label: 'Active', value: 'active', color: '#0A0' },
{ label: 'Closed', value: 'closed', color: '#A00' },
],
}),
// Multi-select
skills: Field.select({
label: 'Skills',
multiple: true,
options: [
{ label: 'JavaScript', value: 'js' },
{ label: 'Python', value: 'python' },
{ label: 'Go', value: 'go' },
],
}),
Options must use { label, value } objects. Values are stored in the database and must be lowercase.
| Type |
Factory |
Description |
lookup |
Field.lookup() |
Many-to-one reference |
master_detail |
Field.masterDetail() |
Parent-child with cascade delete |
// Simple lookup
account: Field.lookup('account', {
label: 'Account',
required: true,
}),
// Filtered lookup — structured filters, cascading from another field
primary_contact: Field.lookup('contact', {
label: 'Primary Contact',
dependsOn: ['account'],
lookupFilters: [
{ field: 'is_active', operator: 'eq', value: true },
],
}),
// Master-detail (cascade delete)
project: Field.masterDetail('project', {
label: 'Project',
required: true,
}),
// Inline line items on the parent form
order: Field.masterDetail('order', {
label: 'Order',
required: true,
inlineEdit: true,
inlineTitle: 'Lines',
inlineAmountField: 'line_total',
}),
| Property |
Type |
Description |
reference (1st arg) |
string |
Target object name |
referenceFilters |
string[] |
Filter conditions for the lookup |
deleteBehavior |
enum |
'set_null', 'cascade', 'restrict' |
inlineEdit |
boolean | 'grid' | 'form' |
Render child records inline on the parent create/edit form (true = auto-pick, 'grid', or 'form') |
inlineColumns |
array |
Optional explicit columns for the inline grid |
inlineAmountField |
string |
Optional numeric child field for the inline running total |
| Type |
Factory |
Description |
image |
Field.image() |
Image file upload |
file |
Field.file() |
Generic file upload |
avatar |
Field.avatar() |
User avatar/profile image |
video |
— |
Video file |
audio |
— |
Audio file |
photo: Field.image({ label: 'Photo' }),
resume: Field.file({ label: 'Resume' }),
profile_image: Field.avatar({ label: 'Profile Image' }),
| Type |
Factory |
Description |
formula |
Field.formula() |
Calculated from other fields |
summary |
Field.summary() |
Roll-up from child records |
autonumber |
Field.autonumber() |
Sequential auto-generated number |
// Formula
total: Field.formula({
label: 'Total',
expression: 'quantity * price * (1 - discount / 100)',
}),
// Summary (roll-up)
total_amount: Field.summary({
label: 'Total Amount',
summaryOperations: {
object: 'order_item',
field: 'amount',
function: 'sum',
relationshipField: 'order',
},
}),
// AutoNumber
case_number: Field.autonumber({
label: 'Case Number',
autonumberFormat: 'CASE-{0000}',
}),
Summary fields are server-owned. The engine recomputes them when child records
are inserted, updated, or deleted. relationshipField is optional when the child
has only one lookup/master_detail field pointing back to the parent; set it
when multiple relationships target the same parent object.
| Type |
Factory |
Description |
address |
Field.address() |
Structured address (street, city, state, country) |
location |
Field.location() |
Geographic coordinates (lat/lng) |
color |
Field.color() |
Color picker (hex/rgb/hsl) |
rating |
Field.rating() |
Star rating (1-5) |
slider |
Field.slider() |
Range slider |
signature |
Field.signature() |
Digital signature capture |
qrcode |
Field.qrcode() |
QR/barcode generator |
code |
Field.code() |
Code editor with syntax highlighting |
json |
Field.json() |
JSON data |
vector |
Field.vector() |
AI embedding vectors |
tags |
— |
Tag list |
progress |
— |
Progress bar |
office: Field.address({ label: 'Office Address' }),
coordinates: Field.location({ label: 'Location' }),
brand_color: Field.color({ label: 'Color' }),
satisfaction: Field.rating(5, { label: 'Rating' }),
approval_signature: Field.signature({ label: 'Signature' }),
embedding: Field.vector(1536, { label: 'Embedding' }),
These properties are available on all field types:
| Property |
Type |
Default |
Description |
required |
boolean |
false |
Field must have a value |
unique |
boolean |
false |
Values must be unique across records |
defaultValue |
any |
— |
Default value for new records |
maxLength |
number |
— |
Maximum character length |
minLength |
number |
— |
Minimum character length |
min |
number |
— |
Minimum numeric value |
max |
number |
— |
Maximum numeric value |
| Property |
Type |
Default |
Description |
label |
string |
— |
Human-readable display name |
description |
string |
— |
Developer documentation |
inlineHelpText |
string |
— |
Help text shown in UI |
hidden |
boolean |
false |
Hide from default views |
readonly |
boolean |
false |
Prevent editing — hidden from create/edit forms AND server-enforced: non-system UPDATE writes to the field are silently dropped (insert may still seed it) |
sortable |
boolean |
true |
Allow sorting by this field |
group |
string |
— |
Group name for organizing in forms (e.g. 'billing') |
| Property |
Type |
Default |
Description |
searchable |
boolean |
false |
Include in full-text search |
index |
boolean |
false |
Create a database index |
externalId |
boolean |
false |
Mark as external system identifier |
| Property |
Type |
Default |
Description |
requiredPermissions |
string[] |
— |
Capabilities required to read/edit this field — masked on read, denied on write unless the caller holds all of them (ADR-0066 D3) |
trackHistory |
boolean |
— |
Render this field's value changes as entries on the record activity timeline |
For values that must be encrypted at rest (API keys, tokens, DB passwords), use the
secret field type — see the Field Type Gallery.
| Property |
Type |
Description |
visibleWhen |
string | Expression |
CEL predicate; field is shown only when TRUE |
readonlyWhen |
string | Expression |
CEL predicate; field is read-only when TRUE |
requiredWhen |
string | Expression |
CEL predicate; field is required when TRUE |
conditionalRequired |
string | Expression |
Deprecated alias of requiredWhen |
multiple |
boolean |
Allow multiple values (for select, lookup, file) |
import { P } from '@objectstack/spec';
import { Field } from '@objectstack/spec/data';
close_reason: Field.select({
label: 'Close Reason',
visibleWhen: P`record.status == 'closed'`,
requiredWhen: P`record.status == 'closed'`,
options: [
{ label: 'Won', value: 'won' },
{ label: 'Lost', value: 'lost' },
{ label: 'Cancelled', value: 'cancelled' },
],
}),
invoice_total: Field.currency({
label: 'Invoice Total',
readonlyWhen: P`record.status == 'paid'`,
}),
Conditional field rules are evaluated in both tiers. ObjectUI forms update
visibility, read-only state, and required markers live as the draft record
changes, including row-scoped rules inside inline master-detail grids. The
server enforces requiredWhen on submit and ignores writes to fields whose
readonlyWhen predicate is TRUE, so client-side affordances are not the
only guard. Use requiredWhen for new metadata; keep conditionalRequired
only for older packages that already emitted it.
- Field names use
snake_case: first_name, annual_revenue, is_active
- Boolean fields use
is_ or has_ prefix: is_active, has_children
- Lookup fields use the object name directly:
account (not account_id)
- Configuration keys use
camelCase: maxLength, defaultValue