Skip to content

Commit ef3e0cc

Browse files
committed
2 parents fbc4b1c + 51f451c commit ef3e0cc

35 files changed

Lines changed: 13135 additions & 2030 deletions

content/docs/objectql/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"title": "Data Protocol",
33
"pages": [
44
"schema",
5+
"state-machine",
56
"types",
67
"query-syntax",
78
"security"

content/docs/objectql/schema.mdx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,16 @@ export const Project = ObjectSchema.create({
174174
apiEnabled: true,
175175
activities: true,
176176
},
177+
178+
stateMachine: {
179+
id: 'project_lifecycle',
180+
initial: 'draft',
181+
states: {
182+
draft: { on: { ACTIVATE: 'active' } },
183+
active: { on: { COMPLETE: 'completed' } },
184+
completed: { type: 'final' }
185+
}
186+
},
177187
178188
validations: [
179189
{
@@ -199,6 +209,7 @@ export const Project = ObjectSchema.create({
199209
| `datasource` | `string` | ❌ | External datasource ID. If omitted, uses default database. |
200210
| `enable` | `object` | ❌ | Feature flags (see [Capabilities](#capabilities)). |
201211
| `fields` | `object` | ❌ | Field definitions (see [Field Definition](#field-definition)). |
212+
| `stateMachine` | `object` | ❌ | Lifecycle state machine definition (see [State Machine](./state-machine.mdx)). |
202213
| `validations` | `array` | ❌ | Business validation rules (see [Validation](#validation-rules)). |
203214
| `triggers` | `array` | ❌ | Database triggers (before/after insert/update/delete). |
204215
| `indexes` | `array` | ❌ | Composite indexes for query optimization. |
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
---
2+
title: State Machine (Lifecycle)
3+
description: Define strict business logic constraints prevents AI hallucinations by enforcing valid transitions.
4+
---
5+
6+
import { Activity } from 'lucide-react';
7+
8+
# State Machine Protocol
9+
10+
The **State Machine Protocol** (`stateMachine`) allows you to define the "Constitution" of a record's lifecycle. It introduces **XState-inspired** state management directly into your ObjectQL definitions.
11+
12+
## Why State Machines?
13+
14+
In the era of **AI Agents**, traditional validation rules are not enough. Large Language Models (LLMs) can "hallucinate" and attempt illogical data updates (e.g., moving a contract from `Draft` directly to `Paid` without `Approval`).
15+
16+
**State Machines provide a hard constraint layer:**
17+
- **Deterministic**: The system rejects any transition not explicitly defined.
18+
- **Self-Documenting**: The workflow is code, not hidden in triggers.
19+
- **AI-Guarded**: Agents must "ask" the state machine for available actions.
20+
21+
## Definition
22+
23+
Add the `stateMachine` property to your `ObjectSchema`.
24+
25+
```typescript
26+
import { ObjectSchema } from '@objectstack/spec/data';
27+
28+
export const PurchaseRequest = ObjectSchema.create({
29+
name: 'purchase_request',
30+
label: 'Purchase Request',
31+
32+
fields: {
33+
status: Field.select({
34+
options: ['draft', 'pending', 'approved', 'rejected']
35+
}),
36+
amount: Field.number(),
37+
},
38+
39+
stateMachine: {
40+
id: 'purchase_lifecycle',
41+
initial: 'draft',
42+
states: {
43+
draft: {
44+
on: {
45+
SUBMIT: {
46+
target: 'pending',
47+
cond: 'amount_valid'
48+
}
49+
},
50+
meta: {
51+
aiInstructions: "Help the user fill out the form. Do verify amount limits."
52+
}
53+
},
54+
pending: {
55+
on: {
56+
APPROVE: 'approved',
57+
REJECT: 'rejected'
58+
},
59+
meta: {
60+
aiInstructions: "Read-only mode. Only analyze risk. Do NOT modify fields."
61+
}
62+
},
63+
approved: {
64+
type: 'final'
65+
},
66+
rejected: {
67+
type: 'final'
68+
}
69+
}
70+
}
71+
});
72+
```
73+
74+
## Structure
75+
76+
### 1. States (`states`)
77+
Each key represents a valid status value for the record.
78+
79+
- **`on`**: Event listeners (Transitions).
80+
- **`meta.aiInstructions`**: Instructions injected into the AI context when the record is in this state.
81+
82+
### 2. Transitions (`on`)
83+
Defines how to move from one state to another.
84+
85+
```typescript
86+
SUBMIT: {
87+
target: 'pending', // Next State
88+
cond: 'amount_valid', // Guard/Condition name
89+
actions: ['notify'] // Side effects
90+
}
91+
```
92+
93+
### 3. Guards (`cond`)
94+
Conditions that must be true for the transition to happen.
95+
96+
### 4. Actions (`actions`)
97+
Side effects (emails, webhooks, field updates) that execute during transition.
98+
99+
## AI Safety Features
100+
101+
This protocol is designed specifically to constrain AI behavior:
102+
103+
1. **Transition Locking**: If an AI Agent tries to update `status` to `approved` while in `draft`, the kernel throws `InvalidTransitionError`.
104+
2. **Context Injection**: The `aiInstructions` are automatically prepended to the Agent's system prompt based on the current record state.
105+
3. **Action Whitelisting**: The Agent can only trigger "Events" (like `SUBMIT`), not raw database updates on protected fields.
106+
107+
## Best Practices: File Structure
108+
109+
For complex business objects (like `Lead`, `Opportunity`, or `Order`), the state machine configuration can grow quite large. To keep your object definitions clean and readable, we **strongly recommend** extracting the state logic into a separate `*.state.ts` file.
110+
111+
### Recommended Pattern
112+
113+
**1. Create `lead.state.ts`**
114+
115+
Define the state machine using `StateMachineConfig` type for full type safety.
116+
117+
```typescript
118+
// src/domains/sales/lead.state.ts
119+
import type { StateMachineConfig } from '@objectstack/spec/automation';
120+
121+
export const LeadStateMachine: StateMachineConfig = {
122+
id: 'lead_lifecycle',
123+
initial: 'new',
124+
states: {
125+
new: {
126+
on: {
127+
QUALIFY: { target: 'qualified' },
128+
DISQUALIFY: { target: 'unqualified' }
129+
}
130+
},
131+
// ... complex logic ...
132+
}
133+
};
134+
```
135+
136+
**2. Import in `lead.object.ts`**
137+
138+
Keep your object definition focused on schema and metadata.
139+
140+
```typescript
141+
// src/domains/sales/lead.object.ts
142+
import { ObjectSchema } from '@objectstack/spec/data';
143+
import { LeadStateMachine } from './lead.state';
144+
145+
export const Lead = ObjectSchema.create({
146+
name: 'lead',
147+
// ... fields ...
148+
stateMachines: {
149+
lifecycle: LeadStateMachine, // Named key for the primary lifecycle
150+
}
151+
});
152+
```
153+
154+
## Multiple State Machines (Parallel Lifecycles)
155+
156+
In real enterprise systems, a single object often has **multiple independent state lines**. For example, an `Order` has:
157+
158+
- **`lifecycle`**`draft → submitted → confirmed → shipped → delivered`
159+
- **`payment`**`unpaid → partial → paid → refunded`
160+
- **`approval`**`pending → approved → rejected`
161+
162+
Use the `stateMachines` (plural) property to define them:
163+
164+
```typescript
165+
// src/domains/sales/order.object.ts
166+
import { ObjectSchema } from '@objectstack/spec/data';
167+
import { OrderLifecycle } from './order-lifecycle.state';
168+
import { OrderPayment } from './order-payment.state';
169+
import { OrderApproval } from './order-approval.state';
170+
171+
export const Order = ObjectSchema.create({
172+
name: 'order',
173+
fields: {
174+
status: Field.select({ options: ['draft', 'submitted', 'confirmed', 'shipped', 'delivered'] }),
175+
payment_status: Field.select({ options: ['unpaid', 'partial', 'paid', 'refunded'] }),
176+
approval_status: Field.select({ options: ['pending', 'approved', 'rejected'] }),
177+
},
178+
stateMachines: {
179+
lifecycle: OrderLifecycle,
180+
payment: OrderPayment,
181+
approval: OrderApproval,
182+
}
183+
});
184+
```
185+
186+
### `stateMachine` vs `stateMachines`
187+
188+
| Property | Type | Use Case |
189+
|:---|:---|:---|
190+
| `stateMachine` | `StateMachineConfig` | Simple objects with a single lifecycle (shorthand) |
191+
| `stateMachines` | `Record<string, StateMachineConfig>` | Complex objects with parallel state lines |
192+
193+
Both can coexist on the same object. The kernel merges them at runtime.

examples/app-crm/src/domains/crm/account.hook.ts

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)