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:
- Field Type Gallery — Choose the right field type
- Query Cheat Sheet — Write powerful queries
- Error Catalog — Handle errors gracefully