Skip to content

Commit 047602e

Browse files
feat(approvals): retire process engine — approval is flow-node only (ADR-0019 P4/P5) (#1408)
* feat(approvals): remove process-driven engine; approval is flow-node only (ADR-0019 P4/P5) Destructive removal of the standalone approval *process* engine. After ADR-0019 P1-P3 made approval work as a durable-pause flow node, this lands A4/A5: the process model is gone and approval exists only as `type: 'approval'` on a Flow. Removed: - spec: ApprovalProcessSchema/Step/Action, approval.form.ts, `approvals` stack collection (metadata-collection, stack.zod, metadata-type-schemas, metadata-form-registry), process_hash pinning - platform-objects: sys_approval_process object + nav + i18n; reframed sys_approval_request to node-era (process_name -> Source, no process_hash) - plugin-approvals: action-executor.ts, process defineProcess/submit/recall, per-object process hooks -> single node-era global record-lock hook; decide() bridges decision -> engine resume; phase-b.test deleted, approval-service.test rewritten to drive the node - rest: /approvals/processes + submit + recall routes (kept request reads + approve/reject decision routes wired to svc.decide) - runtime: app-plugin process seeder - cli/metadata: `approvals` stats + plural mapping Migrated examples off `approvals:` to approval-node flows: - app-crm: DiscountApprovalFlow - app-showcase: BudgetApprovalFlow connector_action is retained as a deliberate ADR-0018 open extension point. Green: spec/platform-objects/plugin-approvals/runtime/rest/cli/metadata/ service-automation + app-crm/app-showcase. * refactor(approvals): single decision path + give CRM example a mirror field Self-review follow-ups on the P4/P5 removal: - Drop decideApprovalNode (approval-node.ts) — it duplicated the resume orchestration now owned by the contract entrypoint ApprovalService.decide(). Migrated approval-node.test.ts to drive decide() via attachAutomation, and removed the now-unused export. One decision/resume path, not two. - Add a readonly approval_status select to crm_opportunity so the Discount Approval flow's approvalStatusField mirror has a real target (the mirror is try/caught, so before this it silently no-op'd in the example). * test(objectql): migrate stale workflow fixture in metadata sweep to state-machine shape The metadata-validation-sweep workflow fixture still used the legacy workflow-rule shape (objectName/triggerType/criteria); `workflow` maps to StateMachineSchema (id/initial/states) since the workflow→state-machine migration, so the valid fixture failed validation. Updated it to a minimal valid state machine. Pre-existing failure surfaced by Test Core; unrelated to the approval node work but fixed here to keep CI green. --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
1 parent c94827d commit 047602e

48 files changed

Lines changed: 794 additions & 3053 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/adr/0019-approval-as-flow-node.md

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0019: Collapse Approval into Flow — one engine, approval as a durable-pause node
22

3-
**Status**: Proposed (2026-05-31)
3+
**Status**: Accepted (2026-05-31) — fully implemented (A1–A5)
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: [ADR-0018](./0018-unified-node-action-registry.md) (open action registry — approval becomes a consumer), [ADR-0009](./0009-execution-pinned-metadata.md) (execution pinning — reconcile to one mechanism), [ADR-0012](./0012-notification-platform.md) (outbox / `notify`), [ADR-0010](./0010-nl-to-flow-authoring.md) + [ADR-0011](./0011-actions-as-ai-tools.md) (AI authoring — the design center)
66
**Revises**: ADR-0018's premise that Approval stays a separate paradigm with its own closed `ApprovalActionType` enum, and the "Workflow-Rule → Flow compiler" (M5) — both dropped here (greenfield, no legacy).
@@ -133,27 +133,33 @@ removal (A4/A5) can be reviewed and sequenced on its own once consumers move ove
133133
`approve` / `reject` edge. New correlation fields on `sys_approval_request`
134134
(`flow_run_id` / `flow_node_id` / `node_config_json`). The standalone process engine is left
135135
intact for the migration window.
136-
4. **A4 — delete parallel pieces.****Follow-up PR (destructive).** Remove
137-
`action-executor.ts`, `ApprovalActionType`, `ApprovalProcessSchema` (top-level) +
138-
`approval.form.ts`; route all actions through the ADR-0018 registry; retire `process_hash`
139-
pinning in favor of Flow pinning. Gated on consumers (CRM examples, API routes, app seeders,
140-
`metadata-type-schemas.ts` / `metadata-form-registry.ts`) migrating off the process model.
141-
5. **A5 — cleanup.****Follow-up PR.** Remove the `workflow_rule` paradigm remnants (the M5
142-
compiler itself was already removed in #1398) and `connector_action` remnants in
143-
`flow.zod.ts`; migrate `approval-service.test.ts` / `phase-b.test.ts` to drive the
144-
Approval node.
145-
146-
> **Landed in this PR:** A1–A3. The engine gained real durable suspend/resume (P1), spec gained
147-
> the Approval node contract (P2), and `plugin-approvals` gained the working node bridge (P3) —
148-
> all additive and green (spec 6605, service-automation 79, plugin-approvals 41). A4/A5 are the
149-
> destructive removal of the now-superseded standalone engine and are deliberately a separate PR.
136+
4. **A4 — delete parallel pieces.****Done (this PR, destructive).** Removed
137+
`action-executor.ts`, `ApprovalActionType`, `ApprovalProcessSchema` / `ApprovalStepSchema` /
138+
`ApprovalActionSchema` (top-level) + `approval.form.ts`, the `sys_approval_process` object,
139+
the `approvals` stack collection, the lifecycle auto-trigger, the REST `/approvals/processes`
140+
+ submit/recall routes, and the app-plugin process seeder; retired `process_hash` pinning in
141+
favor of Flow pinning. All actions now route through the ADR-0018 registry. Consumers (CRM /
142+
showcase examples, API routes, app seeders, `metadata-type-schemas.ts` /
143+
`metadata-form-registry.ts`, CLI / metadata stats) migrated off the process model.
144+
5. **A5 — cleanup.****Done (this PR).** The M5 compiler was already removed in #1398; the
145+
`workflow_rule` paradigm remnants are gone with the process engine. `connector_action` is
146+
**retained** — it is a deliberate open extension point on the ADR-0018 registry, not a process
147+
remnant. `approval-service.test.ts` rewritten to drive the Approval node; `phase-b.test.ts`
148+
deleted.
149+
150+
> **Landed across two PRs:** A1–A3 (additive foundation) shipped first — the engine gained real
151+
> durable suspend/resume (P1), spec gained the Approval node contract (P2), and `plugin-approvals`
152+
> gained the working node bridge (P3). **This PR lands A4–A5**: the destructive removal of the
153+
> now-superseded standalone process engine. Approval exists *only* as a flow node. Green across
154+
> spec / platform-objects / plugin-approvals / runtime / rest / cli / metadata and both example apps.
150155
151156
## Migration map
152157

153158
| Asset | Disposition |
154159
|:---|:---|
155160
| `plugin-approvals` execution loop + `action-executor.ts` | **Delete** (engine + actions now Flow's) |
156-
| `ApprovalActionType`, `connector_action` remnants, M5 compiler | **Delete** |
161+
| `ApprovalActionType`, M5 compiler (`workflow_rule`) | **Delete** |
162+
| `connector_action` | **Keep** — deliberate open extension point (ADR-0018), not a process remnant |
157163
| `ApprovalProcessSchema`, `approval.form.ts` (top-level authoring type) | **Deprecate / remove** — concepts → Approval node config + Flow graph |
158164
| `ApproverType`, `behavior`, `escalation`, `lockRecord`, `approvalStatusField` | **Re-home** → Approval node config schema |
159165
| Approver resolution (team/dept BFS/manager/role/queue) | **Keep** (move under node, ~verbatim) |

examples/app-crm/objectstack.config.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import * as emails from './src/emails/index.js';
1313
import { allHooks } from './src/hooks/index.js';
1414
import { allFlows } from './src/flows/index.js';
1515
import { HighValueDealWorkflow, StaleOpportunityWorkflow } from './src/workflows/index.js';
16-
import { DiscountApprovalProcess } from './src/approvals/index.js';
1716
import {
1817
SalesAssistantAgent,
1918
LookupContactTool,
@@ -105,7 +104,6 @@ export default defineStack({
105104
hooks: allHooks,
106105
flows: allFlows,
107106
workflows: [HighValueDealWorkflow, StaleOpportunityWorkflow],
108-
approvals: [DiscountApprovalProcess],
109107
jobs: [LeadScoringJob, PipelineReportJob, RenewalSweepJob],
110108
emailTemplates: Object.values(emails),
111109

examples/app-crm/src/approvals/discount-approval.approval.ts

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

examples/app-crm/src/approvals/index.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { Flow } from '@objectstack/spec/automation';
4+
5+
/**
6+
* Discount approval — ADR-0019 approval-as-flow-node.
7+
*
8+
* What used to be a standalone two-step approval *process* is now an ordinary
9+
* autolaunched flow with two `approval` nodes. The flow suspends on each
10+
* approval and resumes down the matching `approve` / `reject` edge:
11+
*
12+
* start → manager_review ──approve──▶ finance_review ──approve──▶ end
13+
* └─reject──▶ rejected └─reject──▶ rejected
14+
*
15+
* Finance only signs off when the discount exceeds 30% — that gate is just a
16+
* decision node on the approve edge out of the manager step.
17+
*/
18+
export const DiscountApprovalFlow: Flow = {
19+
name: 'crm_discount_approval',
20+
label: 'Opportunity Discount Approval',
21+
description: 'Two-step approval for opportunities with significant discounts.',
22+
type: 'autolaunched',
23+
24+
nodes: [
25+
{
26+
id: 'start',
27+
type: 'start',
28+
label: 'On Discount Above Threshold',
29+
config: {
30+
objectName: 'crm_opportunity',
31+
triggerType: 'record-after-update',
32+
condition: 'discount_percent > 20',
33+
},
34+
},
35+
{
36+
id: 'manager_review',
37+
type: 'approval',
38+
label: 'Manager Review',
39+
config: {
40+
approvers: [{ type: 'role', value: 'sales_manager' }],
41+
behavior: 'first_response',
42+
lockRecord: true,
43+
approvalStatusField: 'approval_status',
44+
},
45+
},
46+
{
47+
id: 'needs_finance',
48+
type: 'decision',
49+
label: 'Discount Above 30%?',
50+
config: { condition: 'discount_percent > 30' },
51+
},
52+
{
53+
id: 'finance_review',
54+
type: 'approval',
55+
label: 'Finance Review',
56+
config: {
57+
approvers: [{ type: 'role', value: 'finance_approver' }],
58+
behavior: 'unanimous',
59+
lockRecord: true,
60+
approvalStatusField: 'approval_status',
61+
},
62+
},
63+
{ id: 'approved', type: 'end', label: 'Approved' },
64+
{ id: 'rejected', type: 'end', label: 'Rejected' },
65+
],
66+
edges: [
67+
{ id: 'e1', source: 'start', target: 'manager_review' },
68+
{ id: 'e2', source: 'manager_review', target: 'needs_finance', label: 'approve' },
69+
{ id: 'e3', source: 'manager_review', target: 'rejected', label: 'reject' },
70+
{ id: 'e4', source: 'needs_finance', target: 'finance_review', label: 'true' },
71+
{ id: 'e5', source: 'needs_finance', target: 'approved', label: 'false' },
72+
{ id: 'e6', source: 'finance_review', target: 'approved', label: 'approve' },
73+
{ id: 'e7', source: 'finance_review', target: 'rejected', label: 'reject' },
74+
],
75+
};

examples/app-crm/src/flows/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { OpportunityWonFlow } from './opportunity-won.flow.js';
44
import { LeadQualificationFlow } from './lead-qualification.flow.js';
55
import { RenewalReminderFlow } from './renewal-reminder.flow.js';
66
import { ConvertLeadScreenFlow } from './convert-lead.flow.js';
7+
import { DiscountApprovalFlow } from './discount-approval.flow.js';
78

89
export { ConvertLeadScreenFlow } from './convert-lead.flow.js';
910

10-
export const allFlows = [OpportunityWonFlow, LeadQualificationFlow, RenewalReminderFlow, ConvertLeadScreenFlow];
11+
export const allFlows = [OpportunityWonFlow, LeadQualificationFlow, RenewalReminderFlow, ConvertLeadScreenFlow, DiscountApprovalFlow];

examples/app-crm/src/objects/opportunity.object.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ export const Opportunity = ObjectSchema.create({
5656
min: 0,
5757
max: 100,
5858
}),
59+
// Mirror target for the Discount Approval flow's approval nodes
60+
// (ADR-0019). The approval runtime writes the request status here; it is
61+
// readonly to users so only the flow drives it.
62+
approval_status: Field.select({
63+
label: 'Approval Status',
64+
readonly: true,
65+
options: [
66+
{ label: 'Pending', value: 'pending', color: '#F59E0B' },
67+
{ label: 'Approved', value: 'approved', color: '#10B981' },
68+
{ label: 'Rejected', value: 'rejected', color: '#EF4444' },
69+
{ label: 'Recalled', value: 'recalled', color: '#94A3B8' },
70+
],
71+
}),
5972
renewal_of: Field.lookup('crm_opportunity', {
6073
label: 'Renewal Of',
6174
}),

examples/app-showcase/objectstack.config.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { allReports } from './src/reports/index.js';
1010
import { allActions } from './src/actions/index.js';
1111
import { ComponentGalleryPage } from './src/pages/index.js';
1212
import { allFlows } from './src/flows/index.js';
13-
import { allApprovals } from './src/approvals/index.js';
1413
import { allWebhooks } from './src/webhooks/index.js';
1514
import { allJobs } from './src/jobs/index.js';
1615
import { allEmails } from './src/emails/index.js';
@@ -85,7 +84,6 @@ export default defineStack({
8584

8685
// Logic
8786
flows: allFlows,
88-
approvals: allApprovals,
8987
jobs: allJobs,
9088
emailTemplates: allEmails,
9189
webhooks: allWebhooks,

examples/app-showcase/src/approvals/index.ts

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

examples/app-showcase/src/coverage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const COVERAGE = {
6767
},
6868
capabilityChains: {
6969
security: 'security/index.ts — roles + permission set (CRUD + FLS + RLS) + sharing + policy',
70-
automation: 'flows/index.ts + approvals/index.ts + webhooks/index.ts + jobs/index.ts + emails/index.ts',
70+
automation: 'flows/index.ts (incl. approval nodes) + webhooks/index.ts + jobs/index.ts + emails/index.ts',
7171
ai: 'agents/index.ts — agent + tool + skill',
7272
},
7373
i18nThemingPortals: {

0 commit comments

Comments
 (0)