HotCRM Logo

Field Type Selection Guide

Complete reference for choosing the right Field type from @objectstack/spec/data for your HotCRM objects

Field Type Selection Guide

Every field in a HotCRM object is defined using Field.* builders from @objectstack/spec/data. This guide covers all available field types, when to use each, and provides real examples from the codebase.

Quick Reference

import { ObjectSchema, Field } from '@objectstack/spec/data';

Decision Tree

Use this flowchart to select the right field type:

  1. Is it a relationship to another object?

    • Optional association → Field.lookup()
    • Required parent-child (cascade delete) → Field.masterDetail()
    • Aggregate child data on parent → Field.summary()
  2. Is it a choice from predefined options?

    • Single choice → Field.select()
    • Multiple choices → Field.select({ multiple: true })
  3. Is it a computed/auto-generated value?

    • Formula from other fields → Field.formula()
    • Auto-incrementing number → Field.autonumber()
  4. What kind of data?

    • Short text → Field.text()
    • Long text → Field.textarea()
    • Whole/decimal number → Field.number()
    • Yes/No → Field.boolean()
    • Date only → Field.date()
    • Date and time → Field.datetime()
    • Money → Field.currency()
    • Percentage → Field.percent()
    • Email address → Field.email()
    • Phone number → Field.phone()
    • Web URL → Field.url()
  5. Is it a file or media?

    • Document/attachment → Field.file()
    • Photo/avatar → Field.image()
  6. Is it geographic data?

    • GPS coordinates → Field.location()
    • Postal address → Field.address()

Relationship Types

Field.lookup() — Optional Association

Creates an optional reference to another object. Records exist independently.

// packages/finance/src/invoice.object.ts
account: Field.lookup('account', { label: 'Account', required: true }),
contract: Field.lookup('contract', { label: 'Contract' }),

When to use: The child record can exist without the parent, or the relationship is informational (e.g., Invoice → Account, Contact → Account).

Note: Even with required: true, a lookup does not cascade delete. Use masterDetail for cascade behavior.

Field.masterDetail() — Required Parent-Child

Creates a required parent-child relationship with cascade delete. The child cannot exist without the parent.

// packages/finance/src/invoice_line.object.ts
invoice: Field.masterDetail('invoice', { label: 'Invoice', required: true }),

// packages/support/src/case_comment.object.ts
case_id: Field.masterDetail('case', { label: 'Case', required: true }),

// packages/products/src/quote_line_item.object.ts
quote_id: Field.masterDetail('quote', { label: 'Quote', required: true }),

// packages/products/src/product_bundle_component.object.ts
bundle: Field.masterDetail('product_bundle', { label: 'Bundle', required: true }),

When to use: The child record has no meaning without the parent (e.g., Invoice Line → Invoice, Case Comment → Case). Deleting the parent deletes all children.

Enables: Field.summary() rollup fields on the parent object.

Field.summary() — Rollup Aggregation

Aggregates data from child records (requires a masterDetail relationship). Defined on the parent object.

// packages/crm/src/account.object.ts
total_opportunities: Field.summary({
  label: 'Total Opportunities',
  summarizedObject: 'opportunity',
  summarizeFunction: 'count'
}),
total_open_opportunities_amount: Field.summary({
  label: 'Total Open Opportunities Amount',
  summarizedObject: 'opportunity',
  summarizeFunction: 'sum',
  summarizedField: 'amount'
}),

// packages/finance/src/invoice.object.ts
line_item_count: Field.summary({
  label: 'Line Item Count',
  summarizedObject: 'invoice_line',
  summarizeFunction: 'count'
}),
calculated_subtotal: Field.summary({
  label: 'Calculated Subtotal',
  summarizedObject: 'invoice_line',
  summarizeFunction: 'sum',
  summarizedField: 'amount'
}),

// packages/products/src/product_bundle.object.ts
component_count: Field.summary({
  label: 'Component Count',
  summarizedObject: 'product_bundle_component',
  summarizeFunction: 'count'
}),

Summary functions: count, sum, min, max

When to use: You need real-time aggregated data from child records (e.g., total invoice amount, number of line items).


Data Types

Field.text() — Short Text

Single-line text input with optional length constraints.

name: Field.text({
  label: 'Account Name',
  required: true,
  unique: true,
  maxLength: 255
}),
account_number: Field.text({
  label: 'Account Number',
  unique: true,
  maxLength: 40
}),

Options: required, unique, maxLength, defaultValue, helpText

Field.textarea() — Long Text

Multi-line text for descriptions, notes, and comments.

description: Field.textarea({
  label: 'Description',
  maxLength: 32000
}),

Field.number() — Numeric

Integer or decimal numbers.

quantity: Field.number({
  label: 'Quantity',
  required: true,
  scale: 2       // Decimal places
}),
number_of_employees: Field.number({
  label: 'Number of Employees'
}),

Options: min, max, scale (decimal places), required

Field.boolean() — Yes/No

True/false toggle.

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

Field.select() — Single Choice

Dropdown selection from predefined options.

type: Field.select({
  label: 'Account Type',
  options: [
    { label: 'Prospect', value: 'prospect' },
    { label: 'Customer', value: 'customer' },
    { label: 'Partner', value: 'partner' },
    { label: 'Competitor', value: 'competitor' }
  ]
}),
status: Field.select({
  label: 'Status',
  defaultValue: 'draft',
  options: [
    { label: 'Draft', value: 'draft' },
    { label: 'Posted', value: 'posted' },
    { label: 'Paid', value: 'paid' },
    { label: 'Void', value: 'void' }
  ]
}),

Field.select({ multiple: true }) — Multi-Select

Multiple selections from predefined options.

// packages/support/src/knowledge_article.object.ts
related_case_types: Field.select({
  label: 'Related Case Types',
  multiple: true,
  options: []
}),

// packages/support/src/sla_policy.object.ts
applicable_priorities: Field.select({
  label: 'Applicable Priorities',
  multiple: true,
  options: []
}),

When to use: Users need to select more than one option (e.g., applicable categories, supported regions).

Field.date() — Date Only

Date without time component.

close_date: Field.date({
  label: 'Close Date',
  required: true
}),
start_date: Field.date({ label: 'Start Date' }),

Field.datetime() — Date and Time

Full timestamp with date and time.

created_date: Field.datetime({
  label: 'Created Date'
}),
last_activity_date: Field.datetime({
  label: 'Last Activity Date'
}),

Field.currency() — Money

Monetary values with currency formatting.

annual_revenue: Field.currency({
  label: 'Annual Revenue'
}),
unit_price: Field.currency({
  label: 'Unit Price'
}),
amount: Field.currency({
  label: 'Amount'
}),

Field.percent() — Percentage

Percentage values (0–100).

probability: Field.percent({
  label: 'Probability'
}),
discount_percent: Field.percent({
  label: 'Discount Percent'
}),

Field.email() — Email Address

Validated email field.

email: Field.email({
  label: 'Email',
  required: true
}),

Field.phone() — Phone Number

Phone number with formatting.

phone: Field.phone({
  label: 'Phone'
}),
mobile_phone: Field.phone({
  label: 'Mobile Phone'
}),

Field.url() — Web URL

Validated URL field rendered as a link.

website: Field.url({
  label: 'Website'
}),

Field.formula() — Computed Value

Read-only field computed from other fields.

full_name: Field.formula({
  label: 'Full Name',
  formula: 'CONCAT(first_name, " ", last_name)',
  returnType: 'text'
}),

Return types: text, number, currency, percent, date, datetime, boolean

Field.autonumber() — Auto-Incrementing

Automatically generated sequential number.

// packages/finance/src/invoice.object.ts
invoice_number: Field.autonumber({
  label: 'Invoice Number',
  format: 'INV-{YYYY}-{000000}'
}),

Format tokens: {YYYY} (year), {MM} (month), {0000} (zero-padded sequence)


Advanced Types

Field.file() — File Attachment

Document or file upload.

// packages/support/src/case.object.ts
attachments: Field.file({ label: 'Attachments' }),

// packages/support/src/knowledge_article.object.ts
attachment: Field.file({ label: 'Attachment' }),

Field.image() — Image Upload

Photo or image upload with preview.

// packages/hr/src/employee.object.ts
photo: Field.image({ label: 'Photo' }),

// packages/products/src/product.object.ts
product_image: Field.image({ label: 'Product Image' }),

Field.location() — GPS Coordinates

Geographic point with latitude and longitude.

// packages/crm/src/account.object.ts
headquarters_location: Field.location({ label: 'Headquarters Location' }),

Field.address() — Postal Address

Structured mailing address with street, city, state, postal code, country.

// packages/crm/src/account.object.ts
billing_address: Field.address({ label: 'Billing Address' }),
shipping_address: Field.address({ label: 'Shipping Address' }),

// packages/hr/src/employee.object.ts
home_address: Field.address({ label: 'Home Address' }),

Common Field Options

These options are available on most field types:

OptionTypeDescription
labelstringDisplay label
requiredbooleanWhether the field is mandatory
uniquebooleanWhether values must be unique across records
defaultValuevariesDefault value for new records
helpTextstringTooltip help text
maxLengthnumberMaximum character length (text fields)
min / maxnumberValue range (number fields)
scalenumberDecimal places (number fields)

Complete Object Example

Here's a full object using multiple field types:

import { ObjectSchema, Field } from '@objectstack/spec/data';

export const InvoiceLine = ObjectSchema.create({
  name: 'invoice_line',
  label: 'Invoice Line',
  pluralLabel: 'Invoice Lines',
  icon: 'list',
  description: 'Line items for an invoice',

  fields: {
    // Relationship: required parent
    invoice: Field.masterDetail('invoice', { label: 'Invoice', required: true }),

    // Relationship: optional association
    product: Field.lookup('product', { label: 'Product' }),

    // Data fields
    description: Field.text({ label: 'Description' }),
    quantity: Field.number({ label: 'Quantity', required: true, scale: 2 }),
    unit_price: Field.currency({ label: 'Unit Price' }),
    amount: Field.currency({ label: 'Amount' })
  }
});

On this page