Data Model
Objects, fields, relationships, validation, indexes — described to AI or written in TypeScript.
Data Model
The data model is the single source of truth for your app. Once an object exists, ObjectOS gives you REST APIs, a Console 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
| Path | Looks like |
|---|---|
| AI Builder (primary) | "Create a support_ticket object with subject, description, priority, status, assignee." |
| Console click-build | Console → 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 Console Task view, and the Console permission row all appear.
Field types
ObjectStack ships ~25 field types. The most-used ones:
Scalars
| Type | What it stores | Helper |
|---|---|---|
text | Short string | Field.text({ maxLength, required }) |
textarea | Long string | Field.textarea(...) |
markdown | Rich text with markdown | Field.markdown(...) |
number | Integer | Field.number({ min, max }) |
decimal | Exact decimal (money, etc.) | Field.decimal({ precision, scale }) |
boolean | True/false | Field.boolean({ defaultValue }) |
date | Calendar date | Field.date(...) |
datetime | Timestamp | Field.datetime(...) |
email | Validated email | Field.email(...) |
url | Validated URL | Field.url(...) |
phone | Validated phone | Field.phone(...) |
json | Arbitrary JSON | Field.json(...) |
Choices
| Type | Use for |
|---|---|
select | Single choice (enum) |
multiselect | Multiple choices |
Relationships
| Type | Cardinality | Helper |
|---|---|---|
lookup | One-to-many (FK) | Field.lookup({ reference: 'sys_user' }) |
masterDetail | One-to-many with cascade delete | Field.masterDetail({ reference: 'order' }) |
Files & media
| Type | What it stores |
|---|---|
file | One file via the storage service |
image | Image file with preview |
Computed / derived
| Type | Behavior |
|---|---|
formula | Computed at read time from a CEL expression |
summary | Aggregate of related records (sum/count/avg) |
autonumber | Sequence (INV-{000001}) |
created, lastModified | System-maintained timestamps |
createdBy, lastModifiedBy | System-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, Console, 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 in Console:
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
trackHistory: true, // History tab + field-level diffs (default true)
feeds: true, // sys_comment / @mentions (default true)
activities: true, // mirror CRUD to sys_activity timeline (default true)
files: true, // Attachments panel (opt-in, default false)
softDelete: true, // tombstone instead of hard delete
},
});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:
| Object | What |
|---|---|
sys_user | User accounts |
sys_org | Organizations / tenants |
sys_member | Org membership |
sys_position, sys_permission_set | RBAC primitives |
sys_audit_log | Audit trail (when audit capability loaded) |
sys_file, sys_attachment | File metadata (when storage loaded) |
sys_comment, sys_activity | Feed / chatter (when feed loaded) |
sys_session, sys_api_key | Auth artifacts |
sys_webhook, sys_webhook_delivery | Webhook 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
- Permissions — gate access to your objects
- Flows / Automation — react to record changes
- API Access — call your generated REST
os explain— print the rendered schema@objectstack/specsource — the schema is the contract; everything here is derived from it
IDE Skills (Claude Code / Cursor / Copilot)
Install ObjectOS skills into your coding agent so Claude Code, Cursor, Copilot, Codex and friends know how to author ObjectOS metadata correctly.
Relationships
Connect objects with lookups and master-detail — cascade rules, filtered pickers, hierarchies, junction objects, and roll-ups.