ObjectStackObjectStack

Common Patterns

Top 10 patterns for building applications with ObjectStack — CRUD, search, auth, realtime, and more

Common Patterns Guide

This guide covers the most common patterns you will use when building applications with ObjectStack. Each pattern includes a complete, copy-pasteable example.

Import: import { defineStack, defineView, defineApp, defineFlow, defineAgent } from '@objectstack/spec'

Before you copy: the examples use short object names (project, order) for readability. In a real project, os validate additionally enforces your project's namespace prefix on every object name (my_app_project) and an explicit sharingModel on every custom object (ADR-0090) — the patterns below include sharingModel; remember to add your prefix. See Validating Metadata.


1. CRUD Object Definition

Define a basic object with common fields for create, read, update, and delete operations.

import { defineStack } from '@objectstack/spec';

export default defineStack({
  objects: [
    {
      name: 'project',
      label: 'Project',
      sharingModel: 'public_read_write',
      fields: {
        title: { label: 'Title', type: 'text', required: true, maxLength: 200 },
        description: { label: 'Description', type: 'textarea' },
        status: { label: 'Status', type: 'select', options: [
          { label: 'Planning', value: 'planning', default: true },
          { label: 'Active', value: 'active' },
          { label: 'Complete', value: 'complete' },
          { label: 'Archived', value: 'archived' }
        ]},
        owner: { label: 'Owner', type: 'lookup', reference: 'user' },
        due_date: { label: 'Due Date', type: 'date' },
        budget: { label: 'Budget', type: 'currency', currencyConfig: { precision: 2, defaultCurrency: 'USD' } },
      }
    }
  ]
});
import { defineStack } from '@objectstack/spec';

export default defineStack({
  objects: {
    project: {
      label: 'Project',
      sharingModel: 'public_read_write',
      fields: {
        title: { label: 'Title', type: 'text', required: true, maxLength: 200 },
        description: { label: 'Description', type: 'textarea' },
        status: { label: 'Status', type: 'select', options: [
          { label: 'Planning', value: 'planning', default: true },
          { label: 'Active', value: 'active' },
          { label: 'Complete', value: 'complete' },
          { label: 'Archived', value: 'archived' }
        ]},
        owner: { label: 'Owner', type: 'lookup', reference: 'user' },
        due_date: { label: 'Due Date', type: 'date' },
        budget: { label: 'Budget', type: 'currency', currencyConfig: { precision: 2, defaultCurrency: 'USD' } },
      },
    },
  },
});

2. Master-Detail Relationship

Create a parent-child relationship between a master and its detail records. Note that master_detail defaults to deleteBehavior: 'set_null'; set deleteBehavior: 'cascade' explicitly if you want child records deleted with the parent.

{
  objects: [
    {
      name: 'order',
      label: 'Order',
      sharingModel: 'public_read',
      fields: {
        order_number: { label: 'Order #', type: 'autonumber', autonumberFormat: 'ORD-{0000}' },
        customer: { label: 'Customer', type: 'lookup', reference: 'contact' },
        total: {
          label: 'Total',
          type: 'summary',
          summaryOperations: {
            object: 'order_line',
            field: 'line_total',
            function: 'sum',
            relationshipField: 'order',
          },
        },
        status: { label: 'Status', type: 'select', options: [
          { label: 'Draft', value: 'draft', default: true },
          { label: 'Submitted', value: 'submitted' },
          { label: 'Fulfilled', value: 'fulfilled' }
        ]},
      }
    },
    {
      // Master-detail child: access is derived from the parent order record
      name: 'order_line',
      label: 'Order Line',
      sharingModel: 'controlled_by_parent',
      fields: {
        order: {
          label: 'Order',
          type: 'master_detail',
          reference: 'order',
          deleteBehavior: 'cascade',
          inlineEdit: true,
          inlineAmountField: 'line_total',
        },
        product: { label: 'Product', type: 'lookup', reference: 'product' },
        quantity: { label: 'Qty', type: 'number', min: 1, required: true },
        unit_price: { label: 'Unit Price', type: 'currency' },
        line_total: { label: 'Line Total', type: 'formula', expression: 'quantity * unit_price' },
      }
    }
  ]
}

3. Search & Filtered List View

Create a list view with pre-configured filters and column display.

import { defineView } from '@objectstack/spec';

export const taskViews = defineView({
  list: {
    type: 'grid',
    data: { provider: 'object', object: 'task' },
    columns: ['title', 'status', 'priority', 'assigned_to', 'due_date'],
    filter: [['status', '!=', 'closed']],
    sort: [
      { field: 'priority', order: 'desc' },
      { field: 'due_date', order: 'asc' }
    ]
  }
});

4. Kanban Board View

Display records as a kanban board grouped by a status field.

import { defineView } from '@objectstack/spec';

export const taskBoard = defineView({
  list: {
    type: 'kanban',
    data: { provider: 'object', object: 'task' },
    columns: ['title', 'assigned_to'],
    kanban: {
      groupByField: 'status',
      columns: ['title', 'assigned_to']
    }
  }
});

5. Application with Navigation

Define an application with a structured navigation menu.

import { defineApp } from '@objectstack/spec';

export const crm = defineApp({
  name: 'crm',
  label: 'CRM',
  description: 'Customer Relationship Management',
  navigation: [
    {
      id: 'nav_contacts',
      type: 'object',
      objectName: 'contact',
      label: 'Contacts',
      icon: 'users'
    },
    {
      id: 'nav_deals',
      type: 'object',
      objectName: 'deal',
      label: 'Deals',
      icon: 'dollar-sign'
    },
    {
      id: 'nav_activities',
      type: 'object',
      objectName: 'activity',
      label: 'Activities',
      icon: 'activity'
    },
    {
      id: 'nav_dashboard',
      type: 'dashboard',
      dashboardName: 'sales_dashboard',
      label: 'Dashboard',
      icon: 'bar-chart'
    }
  ]
});

6. Automated Flow (Record-Triggered)

Create an automation that fires when a record is created or updated.

import { defineFlow } from '@objectstack/spec';

export const assignmentNotification = defineFlow({
  name: 'task_assignment_notification',
  label: 'Task Assignment Notification',
  type: 'record_change',
  nodes: [
    { id: 'start', type: 'start', label: 'Start' },
    {
      id: 'notify',
      type: 'notify',
      label: 'Notify Assignee',
      config: {
        // String fields use single-brace {…} templates, not {{…}}
        to: '{record.assigned_to.email}',
        subject: 'Task Assigned: {record.title}',
        body: 'You have been assigned to task "{record.title}".'
      }
    },
    { id: 'end', type: 'end', label: 'End' }
  ],
  edges: [
    // Edge conditions are bare CEL (ADR-0032) — no {…} braces.
    { id: 'e1', source: 'start', target: 'notify', condition: 'record.assigned_to != previous.assigned_to' },
    { id: 'e2', source: 'notify', target: 'end' }
  ]
});

7. Approval Flow

Define a multi-step approval flow for records.

{
  objects: [{
    name: 'expense_report',
    label: 'Expense Report',
    sharingModel: 'private',
    fields: {
      title: { label: 'Title', type: 'text', required: true },
      amount: { label: 'Amount', type: 'currency' },
      status: { label: 'Status', type: 'select', options: [
        { label: 'Draft', value: 'draft', default: true },
        { label: 'Submitted', value: 'submitted' },
        { label: 'Approved', value: 'approved' },
        { label: 'Rejected', value: 'rejected' }
      ]},
      submitted_by: { label: 'Submitted By', type: 'lookup', reference: 'user' },
      approved_by: { label: 'Approved By', type: 'lookup', reference: 'user' },
    },
    enable: {
      trackHistory: true,
      apiEnabled: true
    }
  }],
  flows: [{
    name: 'expense_approval',
    label: 'Expense Approval',
    type: 'record_change',
    nodes: [
      { id: 'start', type: 'start', label: 'Start' },
      { id: 'check_amount', type: 'decision', label: 'Check Amount' },
      { id: 'auto_approve', type: 'update_record', label: 'Auto Approve' },
      { id: 'request_approval', type: 'notify', label: 'Request Approval' },
      { id: 'end', type: 'end', label: 'End' }
    ],
    edges: [
      { id: 'e1', source: 'start', target: 'check_amount' },
      // Branch conditions are bare CEL (ADR-0032) — reference fields directly,
      // no {…} template braces.
      { id: 'e2', source: 'check_amount', target: 'auto_approve', condition: 'record.amount < 100' },
      { id: 'e3', source: 'check_amount', target: 'request_approval', condition: 'record.amount >= 100' },
      { id: 'e4', source: 'auto_approve', target: 'end' },
      { id: 'e5', source: 'request_approval', target: 'end' }
    ]
  }]
}

8. AI Agent Configuration

Configure an AI agent with specific tools and instructions.

import { defineAgent } from '@objectstack/spec';

export const supportAgent = defineAgent({
  name: 'support_agent',
  label: 'Customer Support Agent',
  role: 'Customer Support Agent',
  model: {
    provider: 'openai',
    model: 'gpt-4o',
    temperature: 0.3,
    maxTokens: 2048
  },
  instructions: `You are a helpful customer support agent. 
    You can search for customer records, look up order status, 
    and create support tickets. Always be polite and professional.`,
  tools: [
    {
      type: 'query',
      name: 'contact',
      description: 'Search customer records'
    },
    {
      type: 'query',
      name: 'order',
      description: 'Look up order status'
    },
    {
      type: 'action',
      name: 'create_support_ticket',
      description: 'Create a support ticket'
    }
  ]
});

9. Pagination Pattern

Implement pagination for large datasets using offset-based or cursor-based pagination.

Offset-Based (Simple)

// Page 1
const page1 = {
  object: 'contact',
  fields: ['id', 'name', 'email'],
  orderBy: [{ field: 'name', order: 'asc' }],
  limit: 25,
  offset: 0
};

// Page 2
const page2 = { ...page1, offset: 25 };

// Page 3
const page3 = { ...page1, offset: 50 };

Cursor-Based (Scalable)

// First page — no cursor yet
const firstPage = {
  object: 'activity',
  fields: ['id', 'type', 'description', 'created_at'],
  orderBy: [{ field: 'created_at', order: 'desc' }],
  limit: 50
};

// Next page — pass the opaque cursor token returned with the previous response
const nextPage = {
  ...firstPage,
  cursor: { /* opaque cursor token from the previous response */ }
};

10. Field-Level Security

Restrict field visibility and editability based on user profiles.

{
  objects: [{
    name: 'employee',
    label: 'Employee',
    sharingModel: 'private',
    fields: {
      name: { label: 'Name', type: 'text', required: true },
      email: { label: 'Email', type: 'email', required: true },
      department: { label: 'Department', type: 'lookup', reference: 'department' },
      salary: { label: 'Salary', type: 'currency',
        hidden: true,  // Hidden from default views
      },
      // `secret` values are stored encrypted and never returned in plain reads
      api_token: { label: 'API Token', type: 'secret' },
    }
  }]
}

Who can actually read or edit a field is governed by permission sets — grant per-object CRUD, then narrow individual fields:

import { definePermissionSet } from '@objectstack/spec';

export const HrRepPermission = definePermissionSet({
  name: 'hr_rep',
  label: 'HR Rep',
  objects: {
    employee: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false },
  },
  fields: {
    // <object>.<field> — field-level security
    'employee.salary': { readable: true, editable: false }, // Read-only
  },
});

Putting It All Together

Combine these patterns into a complete defineStack() configuration:

import { defineStack } from '@objectstack/spec';

export default defineStack({
  objects: [
    // Pattern 1: CRUD objects
    { name: 'project', label: 'Project', fields: {/* ... */} },
    // Pattern 2: Master-detail
    { name: 'task', label: 'Task', fields: {/* ... */} },
  ],
  views: [
    // Pattern 3: List views
    // Pattern 4: Kanban boards
  ],
  apps: [
    // Pattern 5: Navigation
  ],
  flows: [
    // Pattern 6: Automations
    // Pattern 7: Approval workflows
  ],
  agents: [
    // Pattern 8: AI agents
  ]
});
import { defineStack } from '@objectstack/spec';

export default defineStack({
  objects: {
    // Pattern 1: CRUD objects
    project: { label: 'Project', fields: {/* ... */} },
    // Pattern 2: Master-detail
    task: { label: 'Task', fields: {/* ... */} },
  },
  apps: {
    // Pattern 5: Navigation
  },
  flows: {
    // Pattern 6: Automations
    // Pattern 7: Approval workflows
  },
  agents: {
    // Pattern 8: AI agents
  },
});

Next Steps:

On this page