ObjectStackObjectStack

Example Apps

Run and explore the built-in example applications to learn ObjectStack hands-on

The monorepo ships three ready-to-run examples in examples/ that progressively demonstrate ObjectStack features — from a simple Todo app to a full CRM and a kitchen-sink reference. For a larger external enterprise reference, see the HotCRM repository.

Read these as reference, then build your own with an agent. Each example is the same kind of typed metadata Claude Code authors for you — so they double as a catalog of patterns to point your agent at ("build me a CRM like app-crm, but for support tickets"). To extend one, describe the change in plain language, let the agent edit the metadata, run os validate, and verify in the Console — the loop in Build with Claude Code.

app-todo

The simplest starting point. One object, basic CRUD, dashboards, seed data.

app-crm

A full CRM example (in-repo): multiple objects, views, apps, and seed data.

app-showcase

The kitchen-sink reference. Exercises most metadata types in one app.

Looking for a production server shape? Build an artifact with os compile, then run it with os start or os serve. The framework repo no longer contains a separate apps/objectos production host.


Quick Run

The fastest way to explore all examples at once:

git clone https://github.com/objectstack-ai/objectstack.git
cd objectstack
pnpm install        # Node 22+, pnpm 8+ (corepack enable)
pnpm build          # REQUIRED — nothing builds the workspace implicitly
pnpm dev:showcase   # or: pnpm dev:todo / pnpm dev:crm

Each script starts one example's dev server. pnpm dev is an alias for pnpm dev:showcase.

pnpm build is not optional. The root dev:* scripts begin with node scripts/check-dev-prereqs.mjs, which refuses to boot an unbuilt workspace — every package's dist/ entry point has to be on disk. There is no postinstall, so pnpm install alone leaves the workspace unbuilt. This is the same sequence README.md documents.

pnpm build still does not produce the Console SPA at /_console/ — that is built separately by pnpm objectui:build.


app-todo — Your First App

Path: examples/app-todo/

The simplest complete ObjectStack application. Perfect for understanding the basics.

What's Inside

examples/app-todo/
├── objectstack.config.ts      # App manifest + metadata wiring
├── src/
│   ├── objects/
│   │   └── task.object.ts     # todo_task object: fields, validations, indexes
│   ├── actions/
│   │   └── task.actions.ts    # Complete, Start, Defer, Delete batch
│   ├── apps/
│   │   └── todo.app.ts        # Navigation: Tasks, Analytics
│   ├── views/                 # List / form views
│   ├── dashboards/
│   │   └── task.dashboard.ts  # Widget grid: stats, charts, lists
│   ├── reports/
│   │   └── task.report.ts     # Tabular + summary reports
│   ├── datasets/              # Saved query datasets for charts
│   ├── flows/
│   │   └── task.flow.ts       # Reminder, escalation & recurrence flows
│   ├── translations/          # Locale bundles
│   └── data/
│       └── index.ts           # Seed data via defineSeed()

Key Concepts Demonstrated

ConceptFileWhat You'll Learn
Object & Fieldstask.object.tsField.text(), Field.select(), field options, indexes
Seed Datasrc/data/index.tsdefineSeed() with upsert mode, 8 sample records
Actionstask.actions.tsScript actions, modal actions with params
App Navigationtodo.app.tsNavigation groups, object links, dashboard links
Automationtask.flow.tsAutolaunched flow with record-triggered logic

Run It

# From a freshly cloned monorepo, build it once first — `objectstack dev` runs
# against the workspace's dist/ output and the root prereq gate does not guard
# this path: pnpm install && pnpm build
cd examples/app-todo
pnpm dev

Code Walkthrough

1. Define an objectsrc/objects/task.object.ts:

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

export const Task = ObjectSchema.create({
  name: 'todo_task',
  sharingModel: 'public_read_write',   // OWD — required on every custom object
  label: 'Task',
  pluralLabel: 'Tasks',
  icon: 'check-square',

  fields: {
    subject: Field.text({
      label: 'Subject',
      required: true,
      searchable: true,
    }),
    status: Field.select({
      label: 'Status',
      required: true,
      options: [
        { label: 'Not Started', value: 'not_started', color: '#808080', default: true },
        { label: 'In Progress', value: 'in_progress', color: '#3B82F6' },
        { label: 'Completed', value: 'completed', color: '#10B981' },
      ],
    }),
    due_date: Field.date({ label: 'Due Date' }),
    priority: Field.select({
      label: 'Priority',
      required: true,
      options: [
        { label: 'Low', value: 'low', color: '#60A5FA', default: true },
        { label: 'Normal', value: 'normal', color: '#10B981' },
        { label: 'High', value: 'high', color: '#F59E0B' },
        { label: 'Urgent', value: 'urgent', color: '#EF4444' },
      ],
    }),
  },
});

2. Wire it upobjectstack.config.ts:

import { defineStack } from '@objectstack/spec';
import * as objects from './src/objects';
import * as actions from './src/actions';
import * as apps from './src/apps';
import { TodoSeedData } from './src/data';

export default defineStack({
  manifest: {
    id: 'com.example.todo',
    namespace: 'todo',
    version: '2.0.0',
    type: 'app',
    name: 'Todo Manager',
  },

  objects: Object.values(objects),
  actions: Object.values(actions),
  apps: Object.values(apps),

  // Seed data authored with defineSeed() in src/data/
  data: TodoSeedData,
});

Seed data is authored separately with defineSeed() and wired in via the data key. For example, src/data/index.ts:

import { defineSeed } from '@objectstack/spec/data';
import { Task } from '../objects/task.object';

const tasks = defineSeed(Task, {
  mode: 'upsert',
  externalId: 'subject',
  records: [
    { subject: 'Learn ObjectStack', status: 'completed', priority: 'high' },
    { subject: 'Build a cool app', status: 'in_progress', priority: 'normal' },
  ],
});

export const TodoSeedData = [tasks];

3. The server auto-detects what you need: ObjectQL engine → SQLite driver → Hono HTTP server → REST API at /api/v1/data/todo_task.


HotCRM — Enterprise Scale (external repo)

Repo: github.com/objectstack-ai/hotcrm

A production-grade CRM demonstrating every ObjectStack feature. Use this as a reference for building real enterprise apps. It lives in a dedicated repository so it can evolve independently of the framework.

What's Inside

hotcrm/
├── objectstack.config.ts
├── src/
│   ├── objects/          # 10 objects: Account, Contact, Lead, Opportunity, ...
│   ├── actions/          # 15+ actions per domain
│   ├── agents/           # 5 AI agents: Sales, Service, Lead Enrichment, ...
│   ├── apis/             # Custom REST endpoints: lead-convert, pipeline-stats
│   ├── apps/             # CRM app with full navigation tree
│   ├── dashboards/       # Executive, Sales, Service dashboards
│   ├── data/             # Seed data for all objects
│   ├── flows/            # 5 automation flows: case escalation, approval, ...
│   ├── security/         # 5 permission sets: admin, sales rep, manager, ...
│   ├── rag/              # 4 RAG pipelines: product info, competitive intel, ...
│   ├── reports/          # 7 reports with tabular, summary, matrix formats
│   └── sharing/          # Sharing rules, business-unit hierarchy, org defaults

Features Showcased

FeatureDescription
10 Business ObjectsAccount, Contact, Lead, Opportunity, Case, Campaign, Product, Quote, Contract, Task
State MachineLead lifecycle: new → contacted → qualified → converted with Lead.state.ts
AI AgentsSales Assistant, Service Agent, Revenue Intelligence, Email Campaign, Lead Enrichment
RAG PipelinesProduct info, competitive intel, sales knowledge, support knowledge
Security Model5 permission sets (Admin, Manager, Rep, Agent, Marketing), sharing rules, business-unit hierarchy
AutomationLead conversion, case escalation, opportunity approval, campaign enrollment, quote generation
Custom APIsPOST /lead-convert, GET /pipeline-stats
Seed Data50+ realistic records across all objects

Run It

git clone https://github.com/objectstack-ai/hotcrm.git
cd hotcrm
pnpm install
pnpm dev        # Starts with auto-detected plugins

Or inspect its metadata without starting a server:

cd hotcrm
os info          # Show object/field/flow/agent counts
os validate      # Check schema correctness
os compile       # Build to dist/objectstack.json

app-showcase — Kitchen-Sink Reference

Path: examples/app-showcase/

A kitchen-sink workspace built for demonstration and debugging. It exercises nearly every metadata type, view type, and chart type in a single app — objects, views, apps, pages, dashboards, reports, datasets, flows, jobs, agents, security profiles, translations, webhooks, and more. Use it as a living reference when you want to see how a particular metadata type is authored.

# From a freshly cloned monorepo, build it once first — `objectstack dev` runs
# against the workspace's dist/ output and the root prereq gate does not guard
# this path: pnpm install && pnpm build
cd examples/app-showcase
pnpm dev

It is also the target of pnpm dev / pnpm dev:showcase from the monorepo root.


Composition Pattern

Compose apps and plugins in objectstack.config.ts, compile them into one artifact, then boot that artifact with the CLI.

import { composeStacks } from '@objectstack/spec';
import CrmApp from '../examples/app-crm/objectstack.config';
import TodoApp from '../examples/app-todo/objectstack.config';

// composeStacks merges objects, apps, and other metadata from each stack.
// The combined manifest is selected per the `manifest` option; the last
// stack's manifest is used by default. Duplicate object names throw unless
// you pass an `objectConflict` strategy ('override' | 'merge').
export default composeStacks([CrmApp, TodoApp]);

Single-valued configuration keys — i18n, api, server, runtimeModule — are neither overridden nor merged: identical declarations pass through, and two stacks declaring different values throw an error naming both stacks. The two apps above ship different i18n.supportedLocales (CRM ['en','zh-CN'], Todo ['en','zh-CN','ja-JP']), so composing them means reconciling the two i18n blocks first — make them identical, or declare i18n in only one of the stacks. Composition never picks a locale set for you: each stack's translations bundles are written against the locales that stack declares.

Short Names Are Canonical

Each app declares a namespace in its manifest, but the short object name is what you use everywhere — in engine.find(), hooks, formulas, lookups, REST URLs, and physical tables. The namespace is internal metadata used only for package provenance and cross-package disambiguation.

AppNamespaceObject namePhysical table
Todotodotodo_tasktodo_task
CRMcrmcrm_accountcrm_account

If two packages contribute objects with the same short name, the registry logs a warning. Resolve the collision by giving one object a package-prefixed short name (the convention the examples follow, e.g. crm_account, todo_task) rather than using FQN strings in user code.

Run It

os compile
OS_ARTIFACT_PATH=./dist/objectstack.json os start

Project Structure Conventions

All examples follow the same pattern. The recommended project layout — used by every example in examples/ — looks like this:

my-app/
├── objectstack.config.ts     # ← Entry point: manifest + metadata wiring
├── package.json
├── tsconfig.json
├── src/
│   ├── objects/              # Data models (required)
│   │   ├── index.ts          # Barrel export
│   │   └── *.object.ts       # One file per object
│   ├── actions/              # Buttons, batch operations (optional)
│   ├── flows/                # Automation logic (optional)
│   ├── apps/                 # Navigation definitions (optional)
│   ├── dashboards/           # Analytics dashboards (optional)
│   ├── reports/              # Report definitions (optional)
│   ├── agents/               # AI agents (optional)
│   └── data/                 # Seed data via defineSeed() (optional)
└── test/                     # Tests

os init scaffolds a minimal starter, not this full tree. It emits objectstack.config.ts plus a single object under src/objects/{namespace}_item.object.ts (e.g. my_app_item.object.ts) with a matching barrel src/objects/index.ts — the same {name}.object.ts spelling the convention list below states. Add the other directories above as your app grows.

Naming conventions:

  • Directories: plural (objects/, actions/, flows/)
  • Files: {name}.object.ts, {name}.actions.ts, {name}.flow.ts
  • Object names: snake_case short names with a package prefix (todo_task, crm_account)
  • Config keys: camelCase (maxLength, defaultValue, pluralLabel)

What's Next

On this page