Anatomy of an ObjectStack App
A tour of the metadata an AI agent writes for you — objects, actions, views, apps — and how to read it so you can verify the result in the Console.
An ObjectStack app is its metadata: a few hundred lines of typed definitions for objects, actions, views, apps, automation, and permissions. In the AI-first workflow, an agent writes these files for you — you rarely type them by hand.
But you do need to read them. In AI development the human's job shifts from writing code to verifying what the agent produced: is the object modelled right, is that predicate scoped correctly, does the app match what you asked for? This page is a tour of the pieces so you can review an agent's work with confidence — and catch the things the validation gate can't.
Want to build something, not just read about the pieces? Follow Build with Claude Code — it runs the whole loop (describe → agent authors → validate → verify) on a live app. New here? The Example Apps are working code you can run today.
What you build
Everything an agent writes falls into one of a few areas — the same way the work divides when you build an app. This is the map to hold in your head when you read a project:
| Area | The metadata you author (src/…) | What ObjectStack derives from it |
|---|---|---|
| Data | Objects, fields, relationships, validation (src/objects) | Database schema, CRUD APIs, type safety |
| Automation | Flows, workflows, triggers, approvals (src/flows) | Event handlers, approval chains, scheduled jobs |
| Interface | Views, apps, dashboards, actions (src/views, src/apps) | The Console UI, navigation, responsive layout |
| Access | Roles, permissions, sharing, row-level security | Middleware, RLS policies, field masking |
| AI | Skills, tools, RAG, MCP exposure (src/skills) | Chat, search indexes, an MCP server |
The through-line: you author intent once as metadata, and the runtime derives the database, the API, the UI, and the AI tool surface from it. That's why an agent can build across all of these in one coherent change — and why reading the metadata tells you everything about how the app behaves.
Reading an object
The object is the heart of every app. Here's a representative one, in the same
ObjectSchema.create + Field.* form the scaffolder and the agent use:
import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Ticket = ObjectSchema.create({
name: 'support_desk_ticket', // ← snake_case, namespace-prefixed
label: 'Ticket',
pluralLabel: 'Tickets',
icon: 'life-buoy',
description: 'A customer support request.',
fields: {
subject: Field.text({ label: 'Subject', required: true, searchable: true, maxLength: 200 }),
description: Field.textarea({ label: 'Description' }),
status: Field.select({
label: 'Status',
required: true,
options: [
{ label: 'Open', value: 'open', color: '#3B82F6', default: true },
{ label: 'Pending', value: 'pending', color: '#F59E0B' },
{ label: 'Resolved', value: 'resolved', color: '#10B981' },
{ label: 'Closed', value: 'closed', color: '#6B7280' },
],
}),
},
sharingModel: 'private', // org-wide default (OWD) — required by the security gate
enable: { apiEnabled: true, searchable: true },
});What to check when you read one:
- Names are
snake_caseand namespace-prefixed (support_desk_ticket) — the scaffolder derives the prefix from your project name. - Field types are
Field.*builders (text,textarea,select,date,lookup, …), not raw strings. Aselect'soptionscarry the exact labels, values, and colors you'll see in the UI — verify these match your intent. required,default,searchableencode behavior the UI and API inherit automatically.sharingModelis the org-wide default for records the user doesn't own — an explicit, authored security decision (privateunless you have a reason otherwise). The validation gate rejects custom objects that omit it.
This one definition powers the REST API, the Console UI, and the MCP tools exposed to AI agents. Define it once, and ObjectStack derives the rest — see Data Modeling for the full field catalog and relationships.
Reading the other pieces
The remaining domains follow the same shape — a typed factory call per file. When reviewing an agent's work, these are the two things most worth a close read:
- Actions (
defineAction) — buttons and bulk operations. Theirvisible/disabledpredicates are CEL, record-scoped:record.status, never a barestatus. A bare reference silently hides the action on every record — the single most common AI mistake, which is exactly whyos validaterejects it. See it in action in Build with Claude Code → the gate. On a row action, also guard the field withhas()—has(record.status) && record.status != 'sent'— because a list row carries only the columns that view projects, and reading an absent one faults and hides the button. See Actions for when the guard needs&& record.x != nullon top. - Views & Apps (
defineView,App.create) — the list/form lenses and the navigation. Reading these tells you what the user will actually see and click.
For the full authoring surface of each, load the matching skill or read UI Metadata and Automation.
Reading a project's layout
Metadata lives under src/, one directory per collection. This map lets you
navigate any ObjectStack project — including one an agent just generated:
support-desk/
├── objectstack.config.ts # defineStack() — the single entry point, wires it all
├── src/
│ ├── objects/ # Data models (required)
│ ├── datasources/ # External database / API connections
│ ├── hooks/ # Record lifecycle logic
│ ├── data/ # Seed records
│ ├── views/ # List / form / kanban lenses
│ ├── pages/ # Standalone custom pages
│ ├── apps/ # Navigation shells
│ ├── actions/ # Buttons, bulk operations
│ ├── dashboards/ # Analytics boards
│ ├── reports/ # Saved analytical queries
│ ├── datasets/ # Semantic-layer datasets
│ ├── flows/ # Automation logic
│ ├── functions/ # Named handler callables (code, not metadata)
│ ├── translations/ # i18n bundles
│ ├── security/ # Permission sets, positions, sharing rules, capabilities
│ ├── docs/ # In-app documentation (Markdown)
│ └── agents/ # AI agents
└── test/ # TestsNo app ships every one of those: it is the union of what examples/app-todo and
examples/app-crm lay out flat, plus the directories os g <type> writes into.
A larger app groups the same collections by domain instead —
examples/app-showcase is the reference for that variant:
app-showcase/
├── objectstack.config.ts
├── src/
│ ├── data/ # objects, extensions, hooks, mappings, analytics, seed
│ ├── ui/ # views, pages, apps, actions, dashboards, reports, datasets
│ ├── automation/ # flows, jobs, webhooks
│ ├── security/ # permission sets, positions, sharing rules, capabilities
│ ├── system/ # apis, books, connectors, datasources, emails, server, translations
│ └── docs/ # In-app documentation (Markdown)
└── test/Each folder has an index.ts barrel that re-exports its metadata; those barrels
are imported into objectstack.config.ts. There is no filename-suffix magic —
metadata is wired in through those explicit imports, so objectstack.config.ts is
the one place that tells you what's actually in the app.
Which is why the directory is a convention and the defineStack() key is the
contract: the two trees above disagree about paths and agree exactly about
keys. So when you are holding a piece of metadata and want to know where it
goes, start from the key —
Where each piece of metadata goes lists
every one of them.
Where each piece of metadata goes
Every piece of metadata in an app arrives through one defineStack() key. This
is the full authorable set, ordered data → interface → automation → integration
→ access → AI — one clause each, and the page to read next:
defineStack() key | Declares | Guide |
|---|---|---|
objects | Business objects — tables, fields, validation | Objects |
objectExtensions | Fields and config merged into an object another package owns | Reference |
datasources | Connections to external databases and APIs | External Datasources |
datasourceMapping | Rules routing a package, namespace or object pattern to a datasource | External Datasources |
data | Seed records loaded at bootstrap | Seed Data |
hooks | Record lifecycle logic on insert / update / delete | Hooks |
mappings | Field mappings for data import and export | Reference |
analyticsCubes | Semantic-layer cubes over the object graph | Reference |
datasets | Query-shaped datasets that charts and reports read from | Analytics |
apps | Navigation shells — which tabs a user sees | Apps |
views | List / form / kanban lenses over an object | Views |
pages | Standalone custom pages | Pages |
dashboards | Chart and metric boards | Dashboards |
reports | Saved analytical queries with grouping and totals | Reference |
actions | Buttons and bulk operations, with CEL visibility | Actions |
translations | i18n bundles for labels and messages | Translations |
docs | In-app Markdown documentation items | Doc Pages |
books | Ordered navigation spines over those doc items | Reference |
flows | Automation and approval graphs | Flows |
jobs | Scheduled and background jobs | Reference |
emailTemplates | Templates the email service resolves by name and locale | Reference |
webhooks | Outbound HTTP notifications | Webhooks |
connectors | External system connectors a flow can dispatch | Connectors |
apis | Declarative REST endpoints under your own namespace | Reference |
positions | Capability-distribution groups | Positions |
permissions | Permission sets — object, field and system grants | Permission Sets |
capabilities | Authorization capabilities this package defines | Reference |
sharingRules | Record-level sharing beyond the org-wide default | Sharing Rules |
agents | AI agents — platform-internal; third parties extend through skills | Agents |
skills | Reusable AI capability bundles — the extension primitive | Skills |
tools | Optional AI-presentation refinement over an action or flow | Reference |
Rows linked to a Reference have no hand-written guide yet — the generated schema page is the authority until one lands.
Not in the table, and why. viewItems and runtimeModule are not authorable
at all (viewItems is z.never(), the machine-assembled channel for
runtime-assembled manifests; runtimeModule is written by objectstack build);
manifest, i18n, api, server, requires, tiers, plugins and
devPlugins configure the stack rather than declare metadata items; and
functions and onEnable are code the runtime calls, not metadata it stores.
How you verify
Reading the metadata is half of verification; running the app is the other half. Two commands, both of which the agent runs for you but you can run yourself:
npx os validate # the gate — schema + CEL predicates + widget bindings (no artifact)
npx os dev --ui # boot the app, then open http://localhost:3000/_console/os validate proves the metadata is well-formed; the Console at /_console/
proves it does what you meant — create a record, watch an action show/hide,
confirm the nav and filters. That visual check is the human's half of every build
loop. See Validating Metadata for what
the gate catches, and Build with Claude Code
for the full describe → author → validate → verify cycle.
Go deeper
Data Modeling
Objects, fields, relationships, and validation rules — the foundation of every app.
Business Logic
Flows, workflows, triggers, and formulas that automate business processes.
Security Model
Profiles, permissions, sharing rules, and row-level security.
AI Capabilities
Agents, RAG, natural-language queries, and the generated MCP tool surface.
Next steps
- Build with Claude Code — Build a real app end-to-end with an agent
- Example Apps — Run the Todo, CRM, and showcase examples
- CLI Reference — All available commands
- Protocol Reference — Complete schema documentation