Skip to content

Commit 38bbdc6

Browse files
os-zhuangclaude
andauthored
docs: three-audience documentation — Use section, builder deep-dives, admin guides (#41)
* docs: add builder deep-dive pages for data, interface, and automation New Build-section pages covering the core-feature gaps against the ObjectStack framework docs: relationships, formulas, validation rules; interface landing, apps & navigation, forms, dashboards, custom pages; automation landing, state-machine workflows, and approval processes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0145MY7RdJr8KuRMyn5oKAR6 * docs: expand administrator docs — Setup console, users & org, access management New Configure pages: Administration landing (Setup console tour and task map), Users & Organization (business units, invitations, teams, service accounts), Managing Access (onboarding/offboarding recipes), Field-Level Security, and Connect AI tools via MCP. Sidebar metas updated across all locales. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0145MY7RdJr8KuRMyn5oKAR6 * docs: add Use section — end-user guide verified against the live Console New top-level Use section for everyday users: console tour, working with records, views, dashboards, approvals, notifications, and profile/settings. Content was verified by driving the showcase app's Console in a browser. Landing page gains a three-audience directory (use / build / administer) and the sidebar orders sections by role. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0145MY7RdJr8KuRMyn5oKAR6 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3f234c1 commit 38bbdc6

46 files changed

Lines changed: 3325 additions & 95 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
---
2+
title: Approvals
3+
description: Route records for human sign-off — without letting the automation quietly bypass row-level security.
4+
---
5+
6+
# Approvals
7+
8+
**An approval is a [flow](/docs/build/automation/flows) with an approval node: the flow pauses until a human approves or rejects, then resumes down the matching branch.** There is no separate approval engine to learn — triggers, branches, and error handling all work exactly as they do in any other flow. What this page adds is the approval node itself, and the two access decisions that matter as much as the routing.
9+
10+
## Who does what
11+
12+
Three roles, deliberately separated:
13+
14+
| Role | Can | Needs |
15+
|---|---|---|
16+
| **Builder** | Author and edit the approval flow | `manage_metadata` (typically Studio users) |
17+
| **Submitter** | Submit records that enter the flow | Normal record access — no automation config |
18+
| **Approver** | Act on approval requests (approve / reject) | A capability granted by their permission set |
19+
20+
End users submit records and act on requests; they never edit the automation.
21+
Keep automation configuration surfaces out of consumer apps.
22+
23+
## As whom does it run — the safety decision
24+
25+
A flow declares `runAs`, and for approvals this is the decision that keeps the
26+
flow from quietly bypassing row-level security:
27+
28+
| `runAs` | Data operations run as | Use when |
29+
|---|---|---|
30+
| `'user'` (default) | The **submitter**, respecting their RLS | The flow only touches records the submitter can already see |
31+
| `'system'` | **Elevated** — bypasses RLS | The flow must read/write records the submitter can't (post to a ledger, notify an approver who owns rows the submitter can't see) |
32+
33+
Declare elevation **explicitly** so it's visible, not accidental. The default
34+
of `'user'` means an approval flow can't silently grant the submitter
35+
cross-tenant or cross-owner reach — elevation is opt-in and auditable.
36+
37+
> **Tip:** A schedule-triggered escalation has no triggering user — it must be
38+
> `runAs: 'system'` to act at all. That's the legitimate case for elevation;
39+
> "`system` everywhere to make it work" is the anti-pattern.
40+
41+
## The approval node
42+
43+
The node declares **who approves** and how their decisions aggregate:
44+
45+
```ts
46+
{
47+
id: 'manager_approval',
48+
type: 'approval',
49+
label: 'Manager Approval',
50+
config: {
51+
approvers: [{ type: 'field', value: 'owner_manager_id' }],
52+
behavior: 'unanimous',
53+
approvalStatusField: 'approval_status',
54+
lockRecord: true,
55+
},
56+
}
57+
```
58+
59+
| Config | What it does |
60+
|---|---|
61+
| `approvers` | Who must decide — a named user, a position, or a user resolved from a record field (e.g. the submitter's manager) |
62+
| `behavior` | How multiple approvers aggregate, e.g. `'unanimous'` |
63+
| `approvalStatusField` | Optional record field the plugin mirrors the request status into |
64+
| `lockRecord` | Lock the record against edits while the request is pending |
65+
66+
> **Warning — `position` vs `role`.** `{ type: 'position', value: 'finance_manager' }`
67+
> routes to the holders of a position (`sys_user_position`). The `role` approver
68+
> type is the org-membership tier (`sys_member.role`: `owner`/`admin`/`member`) —
69+
> a position name authored as `type: 'role'` matches nobody and the request
70+
> stalls. `os lint` flags this (`approval-role-not-membership-tier`).
71+
72+
## A complete approval flow
73+
74+
Route large proposals to the owner's manager, then branch on the decision:
75+
76+
```ts
77+
export const opportunityApproval = defineFlow({
78+
name: 'opportunity_approval',
79+
label: 'Opportunity Approval',
80+
type: 'record_change',
81+
status: 'active',
82+
runAs: 'user', // submitter's RLS unless a step genuinely needs more
83+
nodes: [
84+
{
85+
id: 'start',
86+
type: 'start',
87+
config: {
88+
triggerType: 'record-after-update',
89+
objectName: 'opportunity',
90+
condition: "record.amount >= 50000 && record.stage == 'proposal'",
91+
},
92+
},
93+
{
94+
id: 'manager_approval',
95+
type: 'approval',
96+
label: 'Manager Approval',
97+
config: {
98+
approvers: [{ type: 'field', value: 'owner_manager_id' }],
99+
behavior: 'unanimous',
100+
approvalStatusField: 'approval_status',
101+
lockRecord: true,
102+
},
103+
},
104+
{ id: 'mark_approved', type: 'update_record', label: 'Mark Approved' },
105+
{ id: 'mark_rejected', type: 'update_record', label: 'Mark Rejected' },
106+
{ id: 'end', type: 'end' },
107+
],
108+
edges: [
109+
{ id: 'e1', source: 'start', target: 'manager_approval' },
110+
{ id: 'approved', source: 'manager_approval', target: 'mark_approved', label: 'approve' },
111+
{ id: 'rejected', source: 'manager_approval', target: 'mark_rejected', label: 'reject' },
112+
{ id: 'e4', source: 'mark_approved', target: 'end' },
113+
{ id: 'e5', source: 'mark_rejected', target: 'end' },
114+
],
115+
});
116+
```
117+
118+
Note the labeled edges: `approve` and `reject` name the post-decision
119+
branches. Always model the rejection path — a flow with only an approve branch
120+
strands rejected records.
121+
122+
**Multi-step approvals:** chain multiple `approval` nodes. **Parallel
123+
approvals:** see the aggregating-node pattern in
124+
[Flows](/docs/build/automation/flows).
125+
126+
## What the approver experiences
127+
128+
The `@objectstack/plugin-approvals` package owns the durable approval state.
129+
While a request is pending:
130+
131+
1. The plugin persists the request (`sys_approval_request`) and every decision
132+
taken on it (`sys_approval_action`) — your approval history is queryable data.
133+
2. With `lockRecord: true`, the record is locked against edits until the
134+
decision lands.
135+
3. If you set `approvalStatusField`, the record's own field mirrors the request
136+
status, so views and reports can filter on it.
137+
4. The approver approves or rejects; the plugin resumes the paused flow down
138+
the matching edge.
139+
140+
Approving is itself a gated action. Model "may approve" as a capability (e.g.
141+
`approve_invoice`) granted by the approver's permission set, and gate the
142+
approve action's `requiredPermissions` on it — the gate is then enforced on
143+
**both** the UI and the server, not just hidden from a screen.
144+
145+
## Best practices
146+
147+
| Do | Don't |
148+
|---|---|
149+
| Define clear entry criteria | Create too many approval steps |
150+
| Set reasonable timeout periods | Make approvals too complex |
151+
| Allow recall when appropriate | Forget rejection paths |
152+
| Notify all stakeholders | Hard-code approvers |
153+
| Track approval history | Gate "Approve" only in the UI |
154+
| Default to `runAs: 'user'`, elevate one step at a time | Set `runAs: 'system'` everywhere "to make it work" |
155+
156+
## Where to go next
157+
158+
| Page | Why |
159+
|---|---|
160+
| [Flows](/docs/build/automation/flows) | The flow reference this page builds on — triggers, steps, error handling |
161+
| [Workflows](/docs/build/automation/workflows) | Constrain the lifecycle the approval sits inside |
162+
| [Actions](/docs/build/interface/actions) | Surface submit and approve as buttons in the interface |
163+
| [CEL expressions](/docs/reference/cel) | The language behind entry conditions |
164+
| [Email](/docs/configure/email) | Notify approvers and stakeholders |
165+
| [Automation overview](/docs/build/automation) | Chooser: flow vs workflow vs approval |
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
title: Automation
3+
description: Pick the right tool for the job — flows for steps, workflows for state, approvals for human sign-off.
4+
---
5+
6+
# Automation
7+
8+
**Automation attaches business logic to your data model declaratively — as metadata the runtime executes — instead of scattering it through application code.** Three tools cover the ground, and picking the right one first saves you from rebuilding later.
9+
10+
## Which one do I need?
11+
12+
| You need | Use | Read |
13+
|---|---|---|
14+
| "When X happens, do Y" — steps that run on a record change, a schedule, or a button click | **Flow** | [Flows](/docs/build/automation/flows) |
15+
| "This record may only move through these states, by these events" — a controlled lifecycle | **Workflow** (state machine) | [Workflows](/docs/build/automation/workflows) |
16+
| "A person must sign off before this proceeds" — routed human decisions | **Approval** | [Approvals](/docs/build/automation/approvals) |
17+
18+
Rule of thumb: model **state** with workflows, model **steps** with flows. Approvals are not a separate engine — an approval is a flow that pauses at an approval node until a human decides.
19+
20+
## How they combine
21+
22+
The three compose rather than compete:
23+
24+
- A **workflow** constrains which lifecycle transitions are valid at all — states, transitions, and guards, nothing else.
25+
- A **flow** performs the side effects around those transitions: send an email, update records, call an external service, wait, branch.
26+
- An **approval node** inside a flow blocks execution until an approver acts, then resumes down the approve or reject branch.
27+
28+
So "a case moves new → assigned → resolved" is a workflow; "notify the manager when it escalates" is a flow; "a discount over $50k needs the finance manager's sign-off" is a flow with an approval node.
29+
30+
> **Tip:** If a request reads "when X happens, do Y", it's a flow. If it reads "this record must never skip a state", it's a workflow. Start there and you'll rarely choose wrong.
31+
32+
## Where to go next
33+
34+
| Page | Why |
35+
|---|---|
36+
| [Flows](/docs/build/automation/flows) | The full flow reference — triggers, step types, error handling, CEL |
37+
| [Workflows](/docs/build/automation/workflows) | States, transitions, and guards for strict lifecycles |
38+
| [Approvals](/docs/build/automation/approvals) | Approval nodes, run-as identity, and the approver experience |
39+
| [CEL expressions](/docs/reference/cel) | The expression language used in conditions and guards |
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
---
2+
title: Workflows
3+
description: Model a record's lifecycle as a state machine — valid states, guarded transitions, and nothing a flow can do better.
4+
---
5+
6+
# Workflows
7+
8+
**A workflow models a record's lifecycle as a finite state machine: the states the record can be in, the events that move it between them, and the guards that must hold for a move to happen.** Use one when the core requirement is "this object can only move through these states by these events."
9+
10+
There is no standalone Salesforce-style Workflow Rule authoring type. The old
11+
"workflow" concept splits cleanly in two:
12+
13+
- **State machine metadata** for strict lifecycle transitions — this page.
14+
- **[Flows](/docs/build/automation/flows)** for event-triggered or scheduled
15+
automation, including [approval](/docs/build/automation/approvals) pauses.
16+
17+
## Workflows vs flows
18+
19+
| | Workflow (state machine) | Flow |
20+
|---|---|---|
21+
| Models | *State* — where a record is in its lifecycle | *Steps* — what happens when something occurs |
22+
| Answers | "Is this transition allowed right now?" | "What do we do about it?" |
23+
| Shape | States, transitions, guards | Nodes and edges: triggers, actions, branches |
24+
| Side effects | None — it only constrains | All of them — email, updates, HTTP, waits |
25+
26+
They compose: the state machine constrains the transitions; flows perform the
27+
side effects around them when you need notifications, record updates, or
28+
external calls.
29+
30+
## Define a state machine
31+
32+
A support case that must move `new → assigned → resolved`, with an escalation
33+
path:
34+
35+
```ts
36+
import type { StateMachineConfig } from '@objectstack/spec/automation';
37+
38+
export const caseLifecycle: StateMachineConfig = {
39+
id: 'case_lifecycle',
40+
initial: 'new',
41+
states: {
42+
new: {
43+
on: {
44+
ASSIGN: { target: 'assigned' },
45+
},
46+
},
47+
assigned: {
48+
on: {
49+
RESOLVE: { target: 'resolved', cond: 'has_resolution' },
50+
ESCALATE: { target: 'escalated' },
51+
},
52+
},
53+
escalated: {
54+
on: {
55+
RESOLVE: { target: 'resolved', cond: 'has_resolution' },
56+
},
57+
},
58+
resolved: {
59+
type: 'final',
60+
},
61+
},
62+
};
63+
```
64+
65+
Read it as a contract: a `new` case can only be assigned. An `assigned` case
66+
can be resolved — but only if the `has_resolution` guard passes — or escalated.
67+
A `resolved` case is final; nothing moves it again.
68+
69+
## Anatomy
70+
71+
| Key | What it declares |
72+
|---|---|
73+
| `id` | The state machine's identifier |
74+
| `initial` | The state every new record starts in |
75+
| `states` | Map of state name → its outgoing transitions |
76+
| `on` | Events this state responds to (`ASSIGN`, `RESOLVE`, …) |
77+
| `target` | The state an event moves the record to |
78+
| `cond` | A guard that must hold for the transition to fire |
79+
| `type: 'final'` | Terminal state — no outgoing transitions |
80+
81+
### Guards
82+
83+
A guard (`cond`) makes a transition conditional: in the example above,
84+
`RESOLVE` only reaches `resolved` when `has_resolution` holds. Guards are how
85+
you encode "you can't close a case without a resolution" as a structural rule
86+
instead of a validation scattered through UI code.
87+
88+
### Events, not field writes
89+
90+
Transitions fire on named **events** (`ASSIGN`, `ESCALATE`), not on arbitrary
91+
status-field edits. That's the point: the machine defines the complete set of
92+
legal moves, and anything not declared is impossible.
93+
94+
> **Tip:** Keep the machine minimal — states, transitions, guards. The moment
95+
> you want "and then send an email", you've left workflow territory: put the
96+
> side effect in a [flow](/docs/build/automation/flows) that reacts to the
97+
> transition.
98+
99+
## Side effects belong in flows
100+
101+
State machines describe valid transitions and guards — nothing else. When a
102+
transition should *do* something (notify sales, stamp a closed date, call an
103+
external system), pair the machine with a flow triggered by the record change:
104+
105+
```ts
106+
export const dealClosedWon = defineFlow({
107+
name: 'deal_closed_won',
108+
type: 'record_change',
109+
nodes: [
110+
{
111+
id: 'start',
112+
type: 'start',
113+
config: {
114+
triggerType: 'record-after-update',
115+
objectName: 'opportunity',
116+
condition: "record.stage == 'closed_won' && previous.stage != 'closed_won'",
117+
},
118+
},
119+
{ id: 'set_closed_date', type: 'update_record', label: 'Set Closed Date' },
120+
{ id: 'notify_sales', type: 'notify', label: 'Notify Sales' },
121+
{ id: 'end', type: 'end' },
122+
],
123+
edges: [
124+
{ id: 'e1', source: 'start', target: 'set_closed_date' },
125+
{ id: 'e2', source: 'set_closed_date', target: 'notify_sales' },
126+
{ id: 'e3', source: 'notify_sales', target: 'end' },
127+
],
128+
});
129+
```
130+
131+
The machine guarantees the deal *reached* `closed_won` legally; the flow
132+
handles what happens next. Conditions like the one above are CEL expressions —
133+
see [CEL](/docs/reference/cel).
134+
135+
## Migrating from workflow rules
136+
137+
If you're coming from a platform with Workflow Rules, here's the mapping:
138+
139+
| Old concept | Current equivalent |
140+
|---|---|
141+
| Workflow Rule | Flow |
142+
| Time trigger | Scheduled flow |
143+
| Field update action | `update_record` node |
144+
| Email alert | `notify` node |
145+
| HTTP call | `http` node |
146+
| Approval Process | Flow with one or more `approval` nodes |
147+
148+
The test: if the old rule was "when X happens, do Y", model it as a small
149+
flow. If it was "this record must move through controlled states", model the
150+
lifecycle as a state machine and use flows for the side effects.
151+
152+
## Where to go next
153+
154+
| Page | Why |
155+
|---|---|
156+
| [Flows](/docs/build/automation/flows) | Side effects around transitions — triggers, steps, error handling |
157+
| [Approvals](/docs/build/automation/approvals) | Human sign-off as a pause inside a flow |
158+
| [CEL expressions](/docs/reference/cel) | The language behind guards and flow conditions |
159+
| [Data modeling](/docs/build/data) | The objects whose lifecycles you're constraining |
160+
| [Automation overview](/docs/build/automation) | Chooser: flow vs workflow vs approval |

0 commit comments

Comments
 (0)