ObjectOS
BuildData

Data Model

Objects, fields, relationships, validation, indexes — described to AI or written in TypeScript.

The data model is the single source of truth for your app. Once an object exists, ObjectOS gives you REST APIs, a generated view, RBAC checkpoints, audit log entries, and AI tool exposure — for free.

Most customers never write the schema by hand. They describe what they need in the AI Builder and the platform creates the objects, fields, indexes, and translations. This page describes the underlying shape — so you understand what the AI is generating and can edit it directly when you want to.

Authoring paths

PathLooks like
AI Builder (primary)"Create a support_ticket object with subject, description, priority, status, assignee."
Click-buildStudio → Objects → New Object → forms
TypeScript (*.object.ts)The TS shown below — typically inside a forked template

All three produce the same schema. The schema is canonical; everything else is derived.

Anatomy of an object

// src/objects/task.ts
import { ObjectSchema, Field } from '@objectstack/spec/data';

export const Task = ObjectSchema.create({
  name: 'todo_task',
  label: 'Task',
  pluralLabel: 'Tasks',
  icon: 'check-square',
  description: 'A single unit of work.',

  fields: {
    subject:     Field.text({ label: 'Subject', required: true, maxLength: 200 }),
    description: Field.markdown({ label: 'Description' }),
    status:      Field.select({
      label: 'Status',
      options: [
        { label: 'To Do',       value: 'todo', default: true },
        { label: 'In Progress', value: 'in_progress' },
        { label: 'Done',        value: 'done' },
      ],
    }),
    due:         Field.date({ label: 'Due' }),
    assignee:    Field.lookup('sys_user', { label: 'Assignee' }),
  },

  enable: {
    trackHistory: true,    // record field changes in audit log
    apiEnabled: true,      // expose REST endpoints (default true)
    feeds: true,           // chatter / comments / @mentions
  },
});

Register it in your stack:

// objectstack.config.ts
import { defineStack } from '@objectstack/spec';
import * as objects from './src/objects';

export default defineStack({
  manifest: { id: 'my.app', namespace: 'myapp', version: '0.1.0', type: 'app', name: 'My App' },
  objects: Object.values(objects),
});

That's all you need. os dev recompiles, and /api/v1/data/todo_task, the generated Task view, and its permission row all appear.

Field types

ObjectStack ships ~25 field types. The most-used ones:

Scalars

TypeWhat it storesHelper
textShort stringField.text({ maxLength, required })
textareaLong stringField.textarea(...)
markdownRich text with markdownField.markdown(...)
numberIntegerField.number({ min, max })
decimalExact decimal (money, etc.)Field.decimal({ precision, scale })
booleanTrue/falseField.boolean({ defaultValue })
dateCalendar dateField.date(...)
datetimeTimestampField.datetime(...)
emailValidated emailField.email(...)
urlValidated URLField.url(...)
phoneValidated phoneField.phone(...)
jsonArbitrary JSONField.json(...)

Choices

TypeUse for
selectSingle choice (enum)
multiselectMultiple choices

Relationships

TypeCardinalityHelper
lookupOne-to-many (FK)Field.lookup({ reference: 'sys_user' })
masterDetailOne-to-many with cascade deleteField.masterDetail({ reference: 'order' })

Files & media

TypeWhat it stores
fileOne file via the storage service
imageImage file with preview

Computed / derived

TypeBehavior
formulaComputed at read time from a CEL expression
summaryAggregate of related records (sum/count/avg)
autonumberSequence (INV-{000001})
created, lastModifiedSystem-maintained timestamps
createdBy, lastModifiedBySystem-maintained user refs

Required / unique / default

Common modifiers on every scalar field:

Field.text({
  label: 'Code',
  required: true,
  unique: true,          // unique constraint enforced at DB level
  defaultValue: '',
  helpText: 'Internal short code',
})

Validation

Inline:

Field.number({ label: 'Quantity', min: 1, max: 9999 })
Field.text({ label: 'SKU', pattern: '^[A-Z]{3}-[0-9]{4}$' })

Object-level rules (cross-field):

ObjectSchema.create({
  name: 'order',
  fields: { /* ... */ },
  validations: [
    {
      name: 'discount_lt_total',
      message: 'Discount cannot exceed total',
      condition: 'discount < total',
    },
  ],
});

Validation runs on every write — REST, the UI, ObjectQL — so there's no "back door."

Indexes & performance

ObjectSchema.create({
  name: 'order',
  fields: { /* ... */ },
  indexes: [
    { fields: ['status', 'created_at'] },
    { fields: ['account', 'created_at'], unique: false },
  ],
});

The driver creates real DB indexes on schema sync.

Field groups

For long forms, group fields:

ObjectSchema.create({
  name: 'task',
  fieldGroups: [
    { key: 'core',     label: 'Task',     icon: 'check-square' },
    { key: 'planning', label: 'Planning', icon: 'calendar' },
    { key: 'meta',     label: 'Metadata', icon: 'info', defaultExpanded: false },
  ],
  fields: {
    subject: Field.text({ label: 'Subject', group: 'core' }),
    due:     Field.date({ label: 'Due',     group: 'planning' }),
  },
});

Capability flags & ownership

ObjectSchema.create({
  name: 'task',
  ownership: 'own',          // 'own' | 'shared' | 'system'
  sharingModel: 'private',   // OWD — custom objects default to private (v13)
  enable: {
    apiEnabled: true,         // generated REST endpoints (default true)
    searchable: true,         // index records for global search (default true)
    trackHistory: true,       // History tab + field-level diffs (opt-in, default false)
    feeds: true,              // sys_comment / @mentions (default true)
    activities: true,         // mirror CRUD to sys_activity timeline (default true)
    files: false,             // Attachments panel (opt-in, default false)
    clone: true,              // record deep cloning (default true)
  },
});

enable is a closed vocabulary — the block is strict, so an unrecognized flag (a typo like feedEnabled, or a key that used to exist) is a parse error naming the key, not a silently dropped setting. The seven flags above are the whole of it, plus one more key:

enable: {
  apiMethods: ['get', 'list', 'create', 'update'],  // no delete over the API
}

apiMethods whitelists which API operations the object exposes. The authorable vocabulary is exactly six primitives — get, list, create, update, delete, bulk. Omit the key for unrestricted access; [] means deny-all. The eight legacy values (upsert, import, export, aggregate, search, history, restore, purge) are derived effective operations, not things you declare: a stored one still parses but is stripped with a warning naming its replacement (upsertcreate + update, export/aggregate/ searchlist, historyget; restore and purge derive from nothing and are simply deleted). Watch one cliff — a whitelist of only legacy values strips to [], which closes the object's API rather than widening it.

There is no soft delete — enable.trash was removed. Earlier docs pointed at enable: { trash: true } as the replacement for the retired object-level softDelete prop. That was wrong in a way worth stating plainly: trash never had a runtime consumer. Every delete has always been a hard delete, so a default-true flag promising a recycle bin was a false affordance — authors wrote trash: false believing they were opting out of a soft delete that never ran. The key (and its neighbour enable.mru) is now rejected at parse time with guidance rather than ignored. Delete it from your source, or run os migrate meta --from 16 to rewrite it for you.

For recoverability, use per-field trackHistory (an audit trail of what changed) or a lifecycle policy. A real recycle bin is not a shipped capability today.

Also removed from the object surface: softDelete, versioning, search (use top-level searchableFields), recordName (use an autonumber field as nameField), keyPrefix, tags, active, and abstractObjectSchema.create throws a located error naming the replacement if you pass one.

Since ObjectStack 14 the enable.* flags are enforced, not advisory: feeds: false rejects comment creation with 403 FEEDS_DISABLED, files must be opted in before sys_attachment rows can be created (403 FILES_DISABLED otherwise), and activities / trackHistory gate the timeline and History tab. The compliance sys_audit_log row is always written regardless of flags.

Data lifecycle (retention)

High-volume objects can declare a lifecycle block so the platform bounds their growth (ObjectStack 14.4, ADR-0057):

ObjectSchema.create({
  name: 'my_event',
  lifecycle: {
    class: 'event',            // 'record' | 'audit' | 'telemetry' | 'transient' | 'event'
    retention: '14d',          // reaper deletes rows past the window
    storage: { rotation: 'weekly' }, // time-sharded tables, O(1) expiry
  },
});

The built-in LifecycleService (disable with OS_LIFECYCLE_DISABLED=1) reaps expired rows, rotates time-sharded tables, and archives audit-class objects. Platform objects such as sys_activity (14 days) and sys_audit_log (90 days hot, then archive) ship with lifecycle declarations you can tune per environment via the lifecycle.retention_overrides setting.

System objects (free with every project)

You don't have to declare these — they're always there:

ObjectWhat
sys_userUser accounts
sys_orgOrganizations / tenants
sys_memberOrg membership
sys_position, sys_permission_setRBAC primitives
sys_audit_logAudit trail (when audit capability loaded)
sys_file, sys_attachmentFile metadata (when storage loaded)
sys_comment, sys_activityFeed / chatter (when feed loaded)
sys_session, sys_api_keyAuth artifacts
sys_webhook, sys_webhook_deliveryWebhook subs (when enabled)

Reference them in lookup fields by name — e.g. Field.lookup({ reference: 'sys_user' }).

Polymorphic platform features

When you enable feeds: true and trackHistory: true, your object automatically participates in:

  • sys_comment (thread_id = <object>:<id>)
  • sys_attachment (parent_object = <object>, parent_id = <id>)
  • sys_activity (timeline)
  • sys_audit_log (field-level diffs)

You don't wire these per object — they're polymorphic on the platform.

Where to go next

On this page