ObjectStackObjectStack

Field Metadata

Configure field types and properties — text, numbers, dates, relationships, files, and more

Field Metadata

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.

Basic Usage

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 }),
  },
});

Field Types

Text Types

TypeFactoryDescription
textField.text()Single-line text
textareaField.textarea()Multi-line text
emailField.email()Email address with validation
urlField.url()URL with validation
phoneField.phone()Phone number
passwordField.password()Masked password input
markdownField.markdown()Markdown editor
htmlField.html()HTML content
richtextField.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' }),

Number Types

TypeFactoryDescription
numberField.number()Integer or decimal
currencyField.currency()Monetary value with currency support
percentField.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',
  },
}),

Date & Time Types

TypeFactoryDescription
dateField.date()Date only
datetimeField.datetime()Date and time
timeTime only
start_date: Field.date({ label: 'Start Date', required: true }),
created_at: Field.datetime({ label: 'Created At', readonly: true }),

Boolean Type

is_active: Field.boolean({ label: 'Active', defaultValue: true }),

Selection Types

TypeFactoryDescription
selectField.select()Single or multi-select dropdown
radioRadio button group
checkboxesCheckbox 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.

Relationship Types

TypeFactoryDescription
lookupField.lookup()Many-to-one reference
master_detailField.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',
}),
PropertyTypeDescription
reference (1st arg)stringTarget object name
referenceFiltersstring[]Filter conditions for the lookup
deleteBehaviorenum'set_null', 'cascade', 'restrict'
inlineEditboolean | 'grid' | 'form'Render child records inline on the parent create/edit form (true = auto-pick, 'grid', or 'form')
inlineColumnsarrayOptional explicit columns for the inline grid
inlineAmountFieldstringOptional numeric child field for the inline running total

File & Media Types

TypeFactoryDescription
imageField.image()Image file upload
fileField.file()Generic file upload
avatarField.avatar()User avatar/profile image
videoVideo file
audioAudio file
photo: Field.image({ label: 'Photo' }),
resume: Field.file({ label: 'Resume' }),
profile_image: Field.avatar({ label: 'Profile Image' }),

Calculation Types

TypeFactoryDescription
formulaField.formula()Calculated from other fields
summaryField.summary()Roll-up from child records
autonumberField.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.

Specialized Types

TypeFactoryDescription
addressField.address()Structured address (street, city, state, country)
locationField.location()Geographic coordinates (lat/lng)
colorField.color()Color picker (hex/rgb/hsl)
ratingField.rating()Star rating (1-5)
sliderField.slider()Range slider
signatureField.signature()Digital signature capture
qrcodeField.qrcode()QR/barcode generator
codeField.code()Code editor with syntax highlighting
jsonField.json()JSON data
vectorField.vector()AI embedding vectors
tagsTag list
progressProgress 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' }),

Common Properties

These properties are available on all field types:

Constraints

PropertyTypeDefaultDescription
requiredbooleanfalseField must have a value
uniquebooleanfalseValues must be unique across records
defaultValueanyDefault value for new records
maxLengthnumberMaximum character length
minLengthnumberMinimum character length
minnumberMinimum numeric value
maxnumberMaximum numeric value

Display

PropertyTypeDefaultDescription
labelstringHuman-readable display name
descriptionstringDeveloper documentation
inlineHelpTextstringHelp text shown in UI
hiddenbooleanfalseHide from default views
readonlybooleanfalsePrevent editing — hidden from create/edit forms AND server-enforced: non-system UPDATE writes to the field are silently dropped (insert may still seed it)
sortablebooleantrueAllow sorting by this field
groupstringGroup name for organizing in forms (e.g. 'billing')

Search & Index

PropertyTypeDefaultDescription
searchablebooleanfalseInclude in full-text search
indexbooleanfalseCreate a database index
externalIdbooleanfalseMark as external system identifier

Security

PropertyTypeDefaultDescription
requiredPermissionsstring[]Capabilities required to read/edit this field — masked on read, denied on write unless the caller holds all of them (ADR-0066 D3)
trackHistorybooleanRender 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.

Conditional Logic

PropertyTypeDescription
visibleWhenstring | ExpressionCEL predicate; field is shown only when TRUE
readonlyWhenstring | ExpressionCEL predicate; field is read-only when TRUE
requiredWhenstring | ExpressionCEL predicate; field is required when TRUE
conditionalRequiredstring | ExpressionDeprecated alias of requiredWhen
multiplebooleanAllow 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.

Naming Conventions

  • 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

On this page