@@ -143,65 +143,65 @@ That's the job of the other layers.
143143### Example: Permission Rules
144144
145145``` typescript
146- // packages/crm/src/permissions/customer.permission.ts
147- import { Permission } from ' @objectstack/spec' ;
148-
149- export const CustomerPermission = Permission ({
150- object: ' customer' ,
151- rules: [
152- {
153- profile: ' sales_rep' ,
154- crud: {
155- create: true ,
156- read: true ,
157- update: true ,
158- delete: false , // Only managers can delete
159- },
160- fieldPermissions: {
161- annual_revenue: { read: true , edit: false }, // Read-only
162- },
146+ // packages/crm/src/permissions/sales_rep.permission.ts
147+ import type { PermissionSet } from ' @objectstack/spec/security' ;
148+
149+ // A permission set is keyed by object name. Each object grants the four
150+ // CRUD verbs (allowCreate / allowRead / allowEdit / allowDelete), and
151+ // optional field-level security keyed by "object.field".
152+ export const SalesRep: PermissionSet = {
153+ name: ' sales_rep' ,
154+ isProfile: true ,
155+ objects: {
156+ customer: {
157+ allowCreate: true ,
158+ allowRead: true ,
159+ allowEdit: true ,
160+ allowDelete: false , // Only managers can delete
163161 },
164- {
165- profile: ' sales_manager' ,
166- crud: {
167- create: true ,
168- read: true ,
169- update: true ,
170- delete: true ,
171- },
172- },
173- ],
174- });
162+ },
163+ fields: {
164+ ' customer.annual_revenue' : { readable: true , editable: false }, // Read-only
165+ },
166+ };
175167```
176168
177169### Example: Workflow Automation
178170
179- ``` typescript
180- // packages/crm/src/workflows/customer.workflow.ts
181- import { Workflow } from ' @objectstack/spec ' ;
171+ Automations are authored with ` defineFlow ` . A flow is a ** node graph ** —
172+ a typed ` start ` / ` end ` plus decision and action nodes wired together by
173+ ` edges ` — driven by a trigger ` type ` (here ` record_change ` ):
182174
183- export const CustomerWorkflow = Workflow ({
184- object: ' customer' ,
185- trigger: ' after_create' ,
186- conditions: [
187- { field: ' annual_revenue' , operator: ' greaterThan' , value: 1000000 },
175+ ``` typescript
176+ // packages/crm/src/flows/high_value_customer.flow.ts
177+ import { defineFlow } from ' @objectstack/spec' ;
178+
179+ export const HighValueCustomer = defineFlow ({
180+ name: ' high_value_customer' ,
181+ label: ' High-Value Customer Routing' ,
182+ type: ' record_change' , // runs when a customer record changes
183+ nodes: [
184+ { id: ' start' , type: ' start' , label: ' Customer created' },
185+ { id: ' assign' , type: ' action' , label: ' Assign to enterprise team' },
186+ { id: ' notify' , type: ' action' , label: ' Alert sales leadership' },
187+ { id: ' end' , type: ' end' , label: ' End' },
188188 ],
189- actions: [
190- {
191- type: ' assign_owner' ,
192- params: { owner: ' enterprise_sales_team' },
193- },
194- {
195- type: ' send_email' ,
196- params: {
197- template: ' high_value_customer_alert' ,
198- to: ' sales-leadership@company.com' ,
199- },
200- },
189+ edges: [
190+ // Only branch when annual_revenue > 1,000,000 (CEL condition)
191+ { id: ' e1' , source: ' start' , target: ' assign' , condition: ' record.annual_revenue > 1000000' },
192+ { id: ' e2' , source: ' assign' , target: ' notify' },
193+ { id: ' e3' , source: ' notify' , target: ' end' },
201194 ],
202195});
203196```
204197
198+ <Callout type = " info" >
199+ The node/edge wiring above is illustrative. For the full set of node types,
200+ trigger semantics, and condition syntax, see the
201+ [ automation skill] ( /docs/protocol/objectos ) and the ` defineFlow ` schema in
202+ ` @objectstack/spec/automation ` .
203+ </Callout >
204+
205205ObjectOS ** orchestrates** these rules at runtime, independent of the data structure or UI.
206206
207207## Layer 3: ObjectUI (View Protocol)
@@ -220,63 +220,47 @@ ObjectOS **orchestrates** these rules at runtime, independent of the data struct
220220### Example: List View
221221
222222``` typescript
223- // packages/crm/src/views/customer_list .view.ts
224- import { ListView } from ' @objectstack/spec' ;
223+ // packages/crm/src/views/customer .view.ts
224+ import { defineView } from ' @objectstack/spec' ;
225225
226- export const CustomerListView = ListView ({
226+ // A view is a container bound to an object; its `list` slot holds the
227+ // list-view config. Columns are field names; sort is { field, order }.
228+ export const CustomerView = defineView ({
227229 object: ' customer' ,
228- label: ' All Customers' ,
229- type: ' grid' ,
230- columns: [
231- { field: ' name' , width: 200 },
232- { field: ' industry' , width: 150 },
233- { field: ' annual_revenue' , width: 150 },
234- { field: ' primary_contact' , width: 180 },
235- ],
236- filters: [
237- { field: ' industry' , operator: ' equals' },
238- { field: ' annual_revenue' , operator: ' greaterThan' },
239- ],
240- defaultSort: { field: ' name' , direction: ' asc' },
230+ list: {
231+ label: ' All Customers' ,
232+ type: ' grid' ,
233+ columns: [' name' , ' industry' , ' annual_revenue' , ' primary_contact' ],
234+ sort: [{ field: ' name' , order: ' asc' }],
235+ },
241236});
242237```
243238
244239### Example: Form View
245240
246241``` typescript
247- // packages/crm/src/views/customer_form .view.ts
248- import { FormView } from ' @objectstack/spec' ;
242+ // packages/crm/src/views/customer .view.ts
243+ import { defineView } from ' @objectstack/spec' ;
249244
250- export const CustomerFormView = FormView ({
245+ // The same view container can also carry a `form` slot. A form is laid out
246+ // with `sections`, each section listing the fields it renders.
247+ export const CustomerView = defineView ({
251248 object: ' customer' ,
252- label: ' Customer Details' ,
253- type: ' tabbed' ,
254- tabs: [
255- {
256- label: ' Overview' ,
257- sections: [
258- {
259- label: ' Company Information' ,
260- fields: [' name' , ' industry' , ' annual_revenue' ],
261- },
262- {
263- label: ' Contact' ,
264- fields: [' primary_contact' ],
265- },
266- ],
267- },
268- {
269- label: ' Related Records' ,
270- sections: [
271- {
272- label: ' Opportunities' ,
273- component: ' related_list' ,
274- object: ' opportunity' ,
275- filter: { customer: ' $recordId' },
276- },
277- ],
278- },
279- ],
249+ form: {
250+ label: ' Customer Details' ,
251+ type: ' tabbed' ,
252+ sections: [
253+ {
254+ label: ' Company Information' ,
255+ columns: 2 ,
256+ fields: [' name' , ' industry' , ' annual_revenue' ],
257+ },
258+ {
259+ label: ' Contact' ,
260+ fields: [' primary_contact' ],
261+ },
262+ ],
263+ },
280264});
281265```
282266
@@ -290,6 +274,13 @@ The UI doesn't "know" the field types. It asks ObjectQL for the schema and rende
290274
291275Let's trace a ** real-world scenario** : A sales rep creates a new high-value customer.
292276
277+ <Callout type = " info" >
278+ The snippets in this walkthrough (` Auth.getCurrentUser ` , ` Permission.check ` ,
279+ ` ObjectQL.getSchema ` , ` driver.insert ` , ` Workflow.getTriggersFor ` , …) are
280+ ** conceptual pseudo-code** that illustrate the control flow between layers.
281+ They are not literal exported APIs — don't search for these exact symbols.
282+ </Callout >
283+
293284### Step 1: User Action (ObjectUI)
294285
295286```
@@ -431,32 +422,45 @@ export const Opportunity = ObjectSchema.create({
431422### 2. ObjectOS: Define Business Rules
432423
433424``` typescript
434- export const OpportunityWorkflow = Workflow ({
435- object: ' opportunity' ,
436- trigger: ' field_update' ,
437- conditions: [
438- { field: ' stage' , operator: ' equals' , value: ' closed_won' },
425+ import { defineFlow } from ' @objectstack/spec' ;
426+
427+ export const OpportunityWon = defineFlow ({
428+ name: ' opportunity_won' ,
429+ label: ' On Opportunity Won' ,
430+ type: ' record_change' ,
431+ nodes: [
432+ { id: ' start' , type: ' start' , label: ' Stage changed' },
433+ { id: ' invoice' , type: ' action' , label: ' Create invoice' },
434+ { id: ' notify' , type: ' action' , label: ' Notify sales team' },
435+ { id: ' end' , type: ' end' , label: ' End' },
439436 ],
440- actions: [
441- { type: ' create_invoice' , params: { object: ' invoice' } },
442- { type: ' send_notification' , params: { to: ' sales_team' } },
437+ edges: [
438+ { id: ' e1' , source: ' start' , target: ' invoice' , condition: " record.stage == 'closed_won'" },
439+ { id: ' e2' , source: ' invoice' , target: ' notify' },
440+ { id: ' e3' , source: ' notify' , target: ' end' },
443441 ],
444442});
445443```
446444
447445### 3. ObjectUI: Define the Kanban View
448446
449447``` typescript
450- export const OpportunityKanban = ListView ({
448+ import { defineView } from ' @objectstack/spec' ;
449+
450+ // For a kanban list, set type: 'kanban' and put the board config under
451+ // `kanban` (groupByField selects the column axis). `columns` is the list
452+ // of card fields. Drag-and-drop between columns is built in.
453+ export const OpportunityBoard = defineView ({
451454 object: ' opportunity' ,
452- type: ' kanban' ,
453- groupBy: ' stage' ,
454- columns: [
455- { field: ' title' },
456- { field: ' amount' },
457- { field: ' customer' },
458- ],
459- enableDragDrop: true ,
455+ list: {
456+ type: ' kanban' ,
457+ columns: [' title' , ' amount' , ' customer' ],
458+ kanban: {
459+ groupByField: ' stage' ,
460+ summarizeField: ' amount' ,
461+ columns: [' title' , ' amount' ],
462+ },
463+ },
460464});
461465```
462466
@@ -546,7 +550,7 @@ The three protocols are **loosely coupled** but **tightly integrated**:
546550
547551## Next Steps
548552
549- - [ ObjectQL: Data Protocol] ( ( /docs/protocol/objectql) ) - Full data protocol specification
553+ - [ ObjectQL: Data Protocol] ( /docs/protocol/objectql ) - Full data protocol specification
550554- [ ObjectUI: UI Protocol] ( /docs/protocol/objectui ) - Full view protocol specification
551555- [ ObjectOS: System Protocol] ( /docs/protocol/objectos ) - Full control protocol specification
552556- [ Developer Guide] ( /docs/getting-started/quick-start ) - Build your first ObjectStack application
0 commit comments