---
title: Metadata-Driven Development
description: Understanding how structured metadata becomes APIs, UI, workflows, and AI-agent tools
---

import { Database, Code, Link, Laptop } from 'lucide-react';

# What is Metadata-Driven Development?

Metadata-driven development is a paradigm shift where **business intent is defined by declarative, analyzable metadata instead of scattered imperative code.**

## The Problem with Code-First

In traditional development, the "Intent" (e.g., *"This field is a required email address"*) is scattered across multiple layers:

1. **Database:** SQL constraints (`NOT NULL`, `CHECK`)
2. **Backend:** ORM validation (TypeORM decorators, Prisma schemas)
3. **Frontend:** UI validation (React Hook Form + Zod)
4. **Documentation:** API specs (OpenAPI/Swagger)
5. **AI Tools:** MCP/tool definitions, function-call schemas, prompts, and guardrails

When a business requirement changes, you must update code in **three, four, or five places**. When AI agents enter the system, they often receive yet another hand-written tool layer that can drift from the real app. This is **Implementation Coupling**.

### Example: Adding a "Phone Number" Field

**Traditional Approach:**

```sql
-- 1. Database migration
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
```

```typescript
// 2. Backend model
class User {
  @Column()
  @IsPhoneNumber()
  phone: string;
}
```

```typescript
// 3. Frontend validation
const schema = z.object({
  phone: z.string().regex(/^\+?[1-9]\d{1,14}$/),
});
```

```yaml
# 4. API documentation
components:
  schemas:
    User:
      properties:
        phone:
          type: string
          pattern: '^\+?[1-9]\d{1,14}$'
```

**4 files to change. 4 places to keep in sync. 4 opportunities for bugs.**

## The Metadata-Driven Solution

ObjectStack centralizes the "Intent" into a **single TypeScript-authored, Zod-validated Protocol Definition**. The implementation layers act as **Runtime Engines** that interpret this protocol.

### Example: The ObjectStack Way

```typescript
// ONE definition (in objectstack.config.ts)
import { ObjectSchema, Field } from '@objectstack/spec/data';

export const User = ObjectSchema.create({
  name: 'user',
  label: 'User',
  icon: 'user',
  
  fields: {
    phone: Field.phone({
      label: 'Phone Number',
      required: true,
    }),
  },
});
```

> **📘 Syntax Rules**: Always use `ObjectSchema.create()` with `Field.*` helpers for strict TypeScript validation and runtime checking. See [Object Definition Rules](#object-definition-rules) below.

From this single definition, ObjectStack automatically:

✅ Generates database schema  
✅ Creates validation rules  
✅ Builds CRUD APIs  
✅ Renders form fields  
✅ Produces API documentation  
✅ Exposes agent-ready tool metadata

## The Three Truths

In metadata-driven development, we embrace three core truths:

### 1. The UI is a Projection

**Traditional:** Build a form component manually  
**ObjectStack:** The form is a *projection* of the schema

The UI doesn't "build" a form; it **projects** the Object schema into visual components.

```typescript
// The schema IS the form
const TaskForm = <FormView object="task" />
// No manual JSX needed
```

### 2. The API is a Consequence

**Traditional:** Write REST endpoints by hand  
**ObjectStack:** APIs are *generated* from the schema

You don't write controllers or routes. ObjectOS *generates* the entire API graph based on your Object definitions and permission rules.

```bash
# Automatically available after defining the object:
GET    /api/v1/task
POST   /api/v1/task
GET    /api/v1/task/:id
PATCH  /api/v1/task/:id
DELETE /api/v1/task/:id
```

### 3. The Schema is the Application

**Traditional:** The "application" is code scattered across many files  
**ObjectStack:** The "application" is a collection of metadata files

Your entire business logic lives in:
- Object definitions (`.object.ts`)
- View configurations (`.view.ts`)
- Workflow rules (`.workflow.ts`)

The Kernel simply **interprets** these definitions.

### 4. The Tool Surface is Generated

**Traditional:** Every AI tool is manually described and wired to custom queries  
**ObjectStack:** Agent tools are generated from the same object, permission, and workflow metadata that powers the app

An agent does not need to guess which query is safe to call. It can inspect the schema, available actions, field constraints, and permission boundaries exposed by the runtime.

## Benefits of Metadata-Driven

### 1. Single Source of Truth

Change the metadata once, everything updates automatically.

```typescript
// Change this:
phone: Field.phone({ required: true })

// To this:
phone: Field.phone({ required: false })

// ✅ Database constraint updates
// ✅ API validation updates
// ✅ UI form updates
// ✅ Documentation updates
```

### 2. Type Safety by Default

All metadata is defined with Zod schemas:

```typescript
// Source: Zod schema
const FieldSchema = z.object({
  name: z.string(),
  type: z.enum(['text', 'number', 'date']),
});

// Derived: TypeScript type
type Field = z.infer<typeof FieldSchema>;

// Derived: JSON Schema (for IDE autocomplete)
const jsonSchema = zodToJsonSchema(FieldSchema);
```

### 3. Technology Agnostic

Because logic is declarative, you can swap implementations:

```
Same Metadata Definition
          ↓
┌─────────┴─────────┐
│                   │
PostgreSQL      MongoDB
Node.js         Python
React           Flutter
```

### 4. ~100× Less Code — Sized for AI Agents

**Traditional:** ~300 lines of code for a simple CRUD feature
**ObjectStack:** ~30 lines of metadata

For a complete enterprise module (CRM, ticketing, approvals, …) the gap typically widens to **roughly two orders of magnitude** — tens of thousands of lines of hand-written CRUD, forms, validation, and API glue collapse into a few hundred lines of typed metadata.

The point isn't lines of code. The point is **fit in an agent's context window.** When the entire business system is small, typed, and declarative, an AI agent can load it end-to-end, reason about every dependency, and safely refactor across data, API, UI, and permissions in a single change. That turns AI from an autocomplete tool into a real co-maintainer.

```typescript
// All you need:
import { ObjectSchema, Field } from '@objectstack/spec/data';

export const Task = ObjectSchema.create({
  name: 'task',
  label: 'Task',
  icon: 'check-square',
  
  fields: {
    title: Field.text({ 
      label: 'Title',
      required: true,
    }),
    
    status: Field.select({
      label: 'Status',
      options: [
        { label: 'To Do', value: 'todo', default: true },
        { label: 'In Progress', value: 'in_progress' },
        { label: 'Done', value: 'done' },
      ],
    }),
    
    assignee: Field.lookup('user', {
      label: 'Assignee',
    }),
  },
});

// That's it. Full CRUD, REST endpoints, typed SDK, MCP tools,
// validation, and permission scaffolding are ready.
```

### 5. Agent-Ready by Construction

Because objects, fields, relationships, permissions, and workflows are explicit, the runtime can expose a bounded tool surface for AI agents:

- Tools map to real business objects and actions.
- Inputs inherit field validation and type information.
- Permissions still apply through RBAC, RLS, and FLS.
- Executions can be audited against the same metadata artifact.

## Real-World Analogy

Think of metadata-driven development like **HTML vs Canvas**:

### HTML (Declarative)
```html
<h1>Hello World</h1>
```
You describe **what** you want. The browser handles **how** to render it.

### Canvas (Imperative)
```javascript
ctx.font = '32px Arial';
ctx.fillText('Hello World', 10, 50);
```
You specify **exactly how** to draw each pixel.

**ObjectStack is the "HTML" of AI-native business applications.**

## When to Use Metadata-Driven

✅ **Use metadata-driven when:**
- Building CRUD-heavy applications
- Need rapid prototyping and iteration
- Want database flexibility (may change backends)
- Building multi-tenant SaaS platforms
- Require strict type safety and validation
- Need AI agents to safely inspect and operate business data

❌ **Don't use metadata-driven when:**
- Building highly custom, pixel-perfect UIs
- Need real-time 3D graphics or games
- The problem domain is too unique for abstraction
- Performance requires hand-optimized algorithms

## Summary

| Aspect | Traditional | Metadata-Driven |
| :--- | :--- | :--- |
| **Definition** | Code in multiple files | Single metadata definition |
| **Changes** | Update 3-4 places | Update once |
| **Type Safety** | Manual synchronization | Automatic from Zod |
| **Flexibility** | Locked to tech stack | Technology agnostic |
| **Boilerplate** | High (300+ lines) | Low (30 lines) |
| **Agent Tools** | Hand-written and drift-prone | Generated from metadata and permissions |

## Object Definition Rules

When defining objects and metadata in ObjectStack, follow these strict rules and principles:

### 1. Always Use `ObjectSchema.create()` with `Field.*` Helpers

**✅ Correct:**
```typescript
import { ObjectSchema, Field } from '@objectstack/spec/data';

export const Account = ObjectSchema.create({
  name: 'account',
  label: 'Account',
  fields: {
    name: Field.text({ required: true }),
    industry: Field.select({
      options: [
        { label: 'Technology', value: 'technology' },
        { label: 'Finance', value: 'finance' },
      ],
    }),
  },
});
```

**❌ Deprecated:**
```typescript
// Old pattern - no runtime validation
const Account: ServiceObject = {
  name: 'account',
  fields: {
    name: { type: 'text', required: true }
  }
};
```

**Why?**
- ✅ **Type Safety**: Compile-time type checking via `z.input<typeof ObjectSchemaBase>`
- ✅ **Runtime Validation**: Zod validates structure at runtime
- ✅ **IDE Autocomplete**: Field helpers provide intelligent code completion
- ✅ **Error Prevention**: Catches typos and invalid configurations immediately

### 2. Naming Conventions

Follow these strict naming conventions for consistency:

| Element | Convention | Examples |
|---------|-----------|----------|
| **Object Names** (machine names) | `snake_case` | `todo_task`, `project_milestone`, `user_profile` |
| **Field Names** (machine names) | `snake_case` | `first_name`, `annual_revenue`, `is_active` |
| **Constant Names** (exports) | `PascalCase` | `TodoTask`, `ProjectMilestone`, `UserProfile` |
| **Configuration Keys** (props) | `camelCase` | `maxLength`, `defaultValue`, `referenceFilters` |

**Example:**
```typescript
// ✅ Correct naming
export const TodoTask = ObjectSchema.create({
  name: 'todo_task',                    // snake_case machine name
  label: 'Todo Task',
  
  fields: {
    due_date: Field.date({              // snake_case field name
      label: 'Due Date',
      defaultValue: null,               // camelCase config key
    }),
  },
});
```

### 3. Select Field Options Must Use Label/Value Objects

**✅ Correct:**
```typescript
status: Field.select({
  label: 'Status',
  options: [
    { label: 'Open', value: 'open', default: true },
    { label: 'In Progress', value: 'in_progress' },
    { label: 'Closed', value: 'closed' },
  ],
}),
```

**❌ Incorrect:**
```typescript
status: Field.select({
  options: ['open', 'in_progress', 'closed'], // Wrong!
}),
```

**Why?** Option values are machine identifiers stored in the database and must be lowercase to avoid case-sensitivity issues in queries.

### 4. Lookup Fields Must Specify Target Object

**✅ Correct:**
```typescript
owner: Field.lookup('user', {
  label: 'Owner',
  required: true,
}),
```

**❌ Incorrect:**
```typescript
owner: Field.lookup({
  object: 'user',  // Wrong property name
}),
```

### 5. Always Include Descriptive Labels

**✅ Correct:**
```typescript
annual_revenue: Field.currency({
  label: 'Annual Revenue',
  scale: 2,
  min: 0,
}),
```

**❌ Avoid:**
```typescript
annual_revenue: Field.currency({
  // Missing label - field name will be used as fallback
}),
```

### 6. Use Enable Flags for Object Capabilities

```typescript
export const Account = ObjectSchema.create({
  name: 'account',
  label: 'Account',
  
  fields: { /* ... */ },
  
  enable: {
    trackHistory: true,     // Enable field history tracking
    searchable: true,       // Include in global search
    apiEnabled: true,       // Expose via REST/GraphQL
    files: true,            // Enable file attachments
    feeds: true,            // Enable activity feed
    activities: true,       // Enable tasks and events
    trash: true,            // Enable soft delete
    mru: true,              // Track Most Recently Used
  },
});
```

### Quick Reference

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

export const ExampleObject = ObjectSchema.create({
  name: 'example_object',           // Required: snake_case
  label: 'Example Object',          // Required: Human-readable
  pluralLabel: 'Example Objects',   // Optional
  icon: 'box',                      // Optional: Lucide icon name
  description: 'Description text',  // Optional
  
  fields: {
    // Text field
    text_field: Field.text({
      label: 'Text Field',
      required: true,
      maxLength: 255,
    }),
    
    // Number field
    number_field: Field.number({
      label: 'Number',
      min: 0,
      max: 100,
    }),
    
    // Currency field
    price: Field.currency({
      label: 'Price',
      scale: 2,
      min: 0,
    }),
    
    // Select field
    status: Field.select({
      label: 'Status',
      options: [
        { label: 'Active', value: 'active', default: true },
        { label: 'Inactive', value: 'inactive' },
      ],
    }),
    
    // Lookup field
    owner: Field.lookup('user', {
      label: 'Owner',
      required: true,
    }),
    
    // Boolean field
    is_active: Field.boolean({
      label: 'Active',
      defaultValue: true,
    }),
    
    // Date field
    due_date: Field.date({
      label: 'Due Date',
    }),
  },
  
  enable: {
    trackHistory: true,
    apiEnabled: true,
  },
});
```

## Next Steps

- [Metadata Lifecycle & HMR](/concepts/metadata-lifecycle) - How writes propagate through the Repository / Change Log / Cache / Registry
- [The Stack](/docs/core-concepts/the-stack) - How the three protocols work together
- [Object Model](/docs/core-concepts/object-model) - Deep dive into the universal object model
- [ObjectQL Protocol](/docs/protocols/objectql) - Learn the data protocol specification
