From 50c68cc9e97645f02f024422181de447afbb337e Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 31 May 2026 18:33:16 +0800 Subject: [PATCH] docs(approvals): sync docs + skill to the flow-node model (ADR-0019) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P4/P5 (#1408) removed the standalone approval-process engine; approval now exists only as a `type: 'approval'` flow node. This brings the docs and the automation skill back in line with that — no code changes. - skills/objectstack-automation/SKILL.md: drop the deleted ApprovalProcess API; rewrite the approval section to the node model (config schema, approve/reject edges, decision via ApprovalService.decide), add `approval` to the Flow node table (19 node types), add the old→new re-home mapping, refresh best practices + CRM blueprint. - README / guides/packages / concepts/packages: fix the plugin one-liner (no more "approval-process engine"/sys_approval_process). - references/*: regenerate auto-docs from current schemas — approval.mdx now shows ApprovalNodeConfig/ApprovalDecision/etc; also syncs other drifted auto-docs whose schemas changed but weren't regenerated (workflow.mdx -> state-machine.mdx, field `secret` type, ADR-0018 open-registry text, …). Historical records left as-is: CHANGELOGs, ROADMAP milestone, ADR 0009/0018. --- README.md | 2 +- content/docs/concepts/packages.mdx | 4 +- content/docs/guides/packages.mdx | 6 +- content/docs/references/api/metadata.mdx | 18 +- content/docs/references/api/protocol.mdx | 52 +++-- .../docs/references/automation/approval.mdx | 57 +++-- content/docs/references/automation/flow.mdx | 20 +- content/docs/references/automation/index.mdx | 1 - content/docs/references/automation/meta.json | 3 +- .../references/automation/node-executor.mdx | 63 +++++- .../docs/references/automation/workflow.mdx | 132 ------------ content/docs/references/data/datasource.mdx | 1 + content/docs/references/data/field.mdx | 1 + .../references/kernel/metadata-plugin.mdx | 61 +----- content/docs/references/ui/action.mdx | 2 +- skills/objectstack-automation/SKILL.md | 198 ++++++++++-------- 16 files changed, 270 insertions(+), 351 deletions(-) delete mode 100644 content/docs/references/automation/workflow.mdx diff --git a/README.md b/README.md index da7031bba0..5fca47be05 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ Cloud, package registry, and project management subcommands (`os projects`, `os | [`@objectstack/plugin-auth`](packages/plugins/plugin-auth) | Authentication plugin (better-auth) | | [`@objectstack/plugin-security`](packages/plugins/plugin-security) | RBAC, Row-Level Security, Field-Level Security | | [`@objectstack/plugin-sharing`](packages/plugins/plugin-sharing) | Record-level sharing — `sys_record_share` + enforcement middleware | -| [`@objectstack/plugin-approvals`](packages/plugins/plugin-approvals) | Multi-step approval engine — `sys_approval_process` + `sys_approval_request` | +| [`@objectstack/plugin-approvals`](packages/plugins/plugin-approvals) | Approval as a flow node — approver resolution, record lock & status mirror over `sys_approval_request` + `sys_approval_action` | | [`@objectstack/plugin-audit`](packages/plugins/plugin-audit) | Audit logging plugin | | [`@objectstack/plugin-email`](packages/plugins/plugin-email) | Pluggable outbound email transport | | [`@objectstack/plugin-webhooks`](packages/plugins/plugin-webhooks) | Outbound webhook delivery — fan-out `data.record.*` events | diff --git a/content/docs/concepts/packages.mdx b/content/docs/concepts/packages.mdx index 9f48104ea3..f312f3fa53 100644 --- a/content/docs/concepts/packages.mdx +++ b/content/docs/concepts/packages.mdx @@ -585,9 +585,9 @@ Framework adapters that bridge ObjectStack's unified `HttpDispatcher` to specifi ### @objectstack/plugin-approvals -**Description:** Approval-process engine driven by `sys_approval_process` / `sys_approval_request` metadata. +**Description:** Contributes the `approval` flow node (ADR-0019) — an approval rides the one automation engine as a durable-pause node, backed by `sys_approval_request` / `sys_approval_action`. There is no separate approval-process engine. -**Key Features:** Multi-step approval chains, recall, escalation, audit trail, per-step assignees and conditions. +**Key Features:** Approver resolution, `first_response` / `unanimous` behavior, record lock, status mirror, per-node SLA escalation, audit trail; multi-step review is successive `approval` nodes on the flow graph. **Use Cases:** Expense, quote, contract, and any workflow requiring human sign-off. diff --git a/content/docs/guides/packages.mdx b/content/docs/guides/packages.mdx index 883fce87fa..746518c913 100644 --- a/content/docs/guides/packages.mdx +++ b/content/docs/guides/packages.mdx @@ -380,10 +380,10 @@ All services implement contracts from `@objectstack/spec/contracts` and are kern ### @objectstack/plugin-approvals -**Approvals Plugin** — Approval-process engine driven by `sys_approval_process` / `sys_approval_request` metadata. +**Approvals Plugin** — Contributes the `approval` flow node (ADR-0019): an approval runs on the one automation engine as a durable-pause node, backed by `sys_approval_request` / `sys_approval_action`. -- **Features**: Multi-step approval chains, recall, escalation, audit trail -- **When to use**: Any workflow that needs human sign-off (expense, quote, contract, …) +- **Features**: Approver resolution (user/role/team/department/manager/field/queue), `first_response` / `unanimous`, record lock, status mirror, per-node SLA escalation, audit trail +- **When to use**: Any flow that needs human sign-off (expense, quote, contract, …) — add an `approval` node and branch on `approve` / `reject` - **README**: [View README](/packages/plugins/plugin-approvals/README.md) ### @objectstack/plugin-sharing diff --git a/content/docs/references/api/metadata.mdx b/content/docs/references/api/metadata.mdx index 6bc4565e07..ecb8f0ae14 100644 --- a/content/docs/references/api/metadata.mdx +++ b/content/docs/references/api/metadata.mdx @@ -50,8 +50,8 @@ the entire platform, including Hono, Next.js, and NestJS adapters. ## TypeScript Usage ```typescript -import { ConceptListResponse, MetadataBulkResponse, MetadataBulkUnregisterRequest, MetadataDeleteResponse, MetadataDependenciesResponse, MetadataDependentsResponse, MetadataEffectiveResponse, MetadataExistsResponse, MetadataExportRequest, MetadataExportResponse, MetadataImportRequest, MetadataImportResponse, MetadataItemResponse, MetadataListResponse, MetadataNamesResponse, MetadataOverlayResponse, MetadataOverlaySaveRequest, MetadataQueryRequest, MetadataQueryResponse, MetadataRegisterRequest, MetadataTypeInfoResponse, MetadataTypesResponse, MetadataValidateRequest, MetadataValidateResponse } from '@objectstack/spec/api'; -import type { ConceptListResponse, MetadataBulkResponse, MetadataBulkUnregisterRequest, MetadataDeleteResponse, MetadataDependenciesResponse, MetadataDependentsResponse, MetadataEffectiveResponse, MetadataExistsResponse, MetadataExportRequest, MetadataExportResponse, MetadataImportRequest, MetadataImportResponse, MetadataItemResponse, MetadataListResponse, MetadataNamesResponse, MetadataOverlayResponse, MetadataOverlaySaveRequest, MetadataQueryRequest, MetadataQueryResponse, MetadataRegisterRequest, MetadataTypeInfoResponse, MetadataTypesResponse, MetadataValidateRequest, MetadataValidateResponse } from '@objectstack/spec/api'; +import { ConceptListResponse, MetadataBulkResponse, MetadataBulkUnregisterRequest, MetadataDeleteResponse, MetadataDependenciesResponse, MetadataDependentsResponse, MetadataEffectiveResponse, MetadataExistsResponse, MetadataExportRequest, MetadataExportResponse, MetadataImportRequest, MetadataImportResponse, MetadataItemResponse, MetadataListResponse, MetadataNamesResponse, MetadataOverlayResponse, MetadataOverlaySaveRequest, MetadataQueryRequest, MetadataQueryResponse, MetadataRegisterRequest, MetadataTypesResponse, MetadataValidateRequest, MetadataValidateResponse } from '@objectstack/spec/api'; +import type { ConceptListResponse, MetadataBulkResponse, MetadataBulkUnregisterRequest, MetadataDeleteResponse, MetadataDependenciesResponse, MetadataDependentsResponse, MetadataEffectiveResponse, MetadataExistsResponse, MetadataExportRequest, MetadataExportResponse, MetadataImportRequest, MetadataImportResponse, MetadataItemResponse, MetadataListResponse, MetadataNamesResponse, MetadataOverlayResponse, MetadataOverlaySaveRequest, MetadataQueryRequest, MetadataQueryResponse, MetadataRegisterRequest, MetadataTypesResponse, MetadataValidateRequest, MetadataValidateResponse } from '@objectstack/spec/api'; // Validate data const result = ConceptListResponse.parse(data); @@ -355,20 +355,6 @@ Metadata query with filtering, sorting, and pagination | **namespace** | `string` | optional | Optional namespace | ---- - -## MetadataTypeInfoResponse - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **success** | `boolean` | ✅ | Operation success status | -| **error** | `Object` | optional | Error details if success is false | -| **meta** | `Object` | optional | Response metadata | -| **data** | `Object` | optional | Type info | - - --- ## MetadataTypesResponse diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index fe5f91a466..a1ea17842f 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -5,29 +5,13 @@ description: Protocol protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs go in content/docs/guides/. */} -ObjectStack Protocol - Zod Schema Definitions +Response for `GET /api/v1/automation/actions` (ADR-0018). -Defines the runtime-validated contract for interacting with ObjectStack metadata and data. +Returns the live action/node registry — the platform's built-in actions plus -Used by API adapters (HTTP, WebSocket, gRPC) to fetch data/metadata without knowing engine internals. +any plugin-contributed ones — backing the designer palette and flow -This protocol enables: - -- Runtime request/response validation at API gateway level - -- Automatic API documentation generation - -- Type-safe RPC communication between microservices - -- Client SDK generation from schemas - -Architecture Alignment: - -- Salesforce: REST API Request/Response schemas - -- Kubernetes: API Resource schemas with runtime validation - -- GraphQL: Schema-first API design +validation. Each entry is a canonical [ActionDescriptorSchema](ActionDescriptorSchema). **Source:** `packages/spec/src/api/protocol.zod.ts` @@ -36,8 +20,8 @@ Architecture Alignment: ## TypeScript Usage ```typescript -import { AiInsightsRequest, AiInsightsResponse, AiNlqRequest, AiNlqResponse, AiSuggestRequest, AiSuggestResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetViewRequest, GetWorkflowConfigRequest, GetWorkflowStateRequest, GetWorkflowStateResponse, HttpFindQueryParams, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, WorkflowApproveRequest, WorkflowApproveResponse, WorkflowRejectRequest, WorkflowRejectResponse, WorkflowState, WorkflowTransitionRequest, WorkflowTransitionResponse } from '@objectstack/spec/api'; -import type { AiInsightsRequest, AiInsightsResponse, AiNlqRequest, AiNlqResponse, AiSuggestRequest, AiSuggestResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetViewRequest, GetWorkflowConfigRequest, GetWorkflowStateRequest, GetWorkflowStateResponse, HttpFindQueryParams, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, WorkflowApproveRequest, WorkflowApproveResponse, WorkflowRejectRequest, WorkflowRejectResponse, WorkflowState, WorkflowTransitionRequest, WorkflowTransitionResponse } from '@objectstack/spec/api'; +import { AiInsightsRequest, AiInsightsResponse, AiNlqRequest, AiNlqResponse, AiSuggestRequest, AiSuggestResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetViewRequest, GetWorkflowConfigRequest, GetWorkflowConfigResponse, GetWorkflowStateRequest, GetWorkflowStateResponse, HttpFindQueryParams, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, WorkflowApproveRequest, WorkflowApproveResponse, WorkflowRejectRequest, WorkflowRejectResponse, WorkflowState, WorkflowTransitionRequest, WorkflowTransitionResponse } from '@objectstack/spec/api'; +import type { AiInsightsRequest, AiInsightsResponse, AiNlqRequest, AiNlqResponse, AiSuggestRequest, AiSuggestResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetViewRequest, GetWorkflowConfigRequest, GetWorkflowConfigResponse, GetWorkflowStateRequest, GetWorkflowStateResponse, HttpFindQueryParams, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, WorkflowApproveRequest, WorkflowApproveResponse, WorkflowRejectRequest, WorkflowRejectResponse, WorkflowState, WorkflowTransitionRequest, WorkflowTransitionResponse } from '@objectstack/spec/api'; // Validate data const result = AiInsightsRequest.parse(data); @@ -119,6 +103,18 @@ const result = AiInsightsRequest.parse(data); | **suggestions** | `Object[]` | ✅ | Suggested values | +--- + +## AutomationActionsResponse + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **actions** | `Object[]` | ✅ | Registered action descriptors (built-in + plugin) | +| **total** | `integer` | ✅ | Number of descriptors returned (after any filters) | + + --- ## AutomationTriggerRequest @@ -739,6 +735,18 @@ const result = AiInsightsRequest.parse(data); | **object** | `string` | ✅ | Object name to get workflow config for | +--- + +## GetWorkflowConfigResponse + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object name | +| **workflows** | `Object[]` | ✅ | Active state-machine workflows for this object | + + --- ## GetWorkflowStateRequest diff --git a/content/docs/references/automation/approval.mdx b/content/docs/references/automation/approval.mdx index 311ee1dcda..cf65c68d04 100644 --- a/content/docs/references/automation/approval.mdx +++ b/content/docs/references/automation/approval.mdx @@ -14,40 +14,63 @@ Approval Step Approver Type ## TypeScript Usage ```typescript -import { ApprovalAction, ApprovalActionType, ApproverType } from '@objectstack/spec/automation'; -import type { ApprovalAction, ApprovalActionType, ApproverType } from '@objectstack/spec/automation'; +import { ApprovalDecision, ApprovalEscalation, ApprovalNodeApprover, ApprovalNodeConfig, ApproverType } from '@objectstack/spec/automation'; +import type { ApprovalDecision, ApprovalEscalation, ApprovalNodeApprover, ApprovalNodeConfig, ApproverType } from '@objectstack/spec/automation'; // Validate data -const result = ApprovalAction.parse(data); +const result = ApprovalDecision.parse(data); ``` --- -## ApprovalAction +## ApprovalDecision + +### Allowed Values + +* `approve` +* `reject` + + +--- + +## ApprovalEscalation ### Properties | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'field_update' \| 'email_alert' \| 'webhook' \| 'script' \| 'connector_action' \| 'inbox_notify'>` | ✅ | | -| **name** | `string` | ✅ | Action name | -| **config** | `Record` | ✅ | Action configuration | -| **connectorId** | `string` | optional | | -| **actionId** | `string` | optional | | +| **enabled** | `boolean` | ✅ | Enable SLA-based escalation for this node | +| **timeoutHours** | `number` | ✅ | Hours before escalation triggers | +| **action** | `Enum<'reassign' \| 'auto_approve' \| 'auto_reject' \| 'notify'>` | ✅ | Action on escalation timeout | +| **escalateTo** | `string` | optional | User id, role, or manager level to escalate to | +| **notifySubmitter** | `boolean` | ✅ | Notify the original submitter on escalation | --- -## ApprovalActionType +## ApprovalNodeApprover -### Allowed Values +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'user' \| 'role' \| 'team' \| 'department' \| 'manager' \| 'field' \| 'queue'>` | ✅ | | +| **value** | `string` | optional | User id / role / team / department / field / queue — per `type` | + + +--- -* `field_update` -* `email_alert` -* `webhook` -* `script` -* `connector_action` -* `inbox_notify` +## ApprovalNodeConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **approvers** | `Object[]` | ✅ | Allowed approvers for this node | +| **behavior** | `Enum<'first_response' \| 'unanimous'>` | ✅ | How to combine multiple approvers | +| **lockRecord** | `boolean` | ✅ | Lock the record from editing while pending | +| **approvalStatusField** | `string` | optional | Business-object field to mirror request status onto | +| **escalation** | `Object` | optional | Per-node SLA escalation | --- diff --git a/content/docs/references/automation/flow.mdx b/content/docs/references/automation/flow.mdx index 6e7c569ad1..8a043f1a90 100644 --- a/content/docs/references/automation/flow.mdx +++ b/content/docs/references/automation/flow.mdx @@ -5,7 +5,23 @@ description: Flow protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs go in content/docs/guides/. */} -Flow Node Types +Flow Node Types — **built-in seed set** (ADR-0018). + +Historically this `z.enum` *gated* `FlowNodeSchema.type`, which made the + +closed protocol reject any plugin-registered node type — defeating the open + +runtime registry (`registerNodeExecutor(type: string)`). Per ADR-0018 the + +gate is removed: `FlowNodeSchema.type` is now a validated `string`, checked + +against the live action registry at `registerFlow()` time, not frozen here. + +`FlowNodeAction` is **retained** as the canonical list of built-in type ids + +(documentation + the seed descriptor set the engine registers at boot). It + +no longer constrains authored flows — plugins extend the vocabulary. **Source:** `packages/spec/src/automation/flow.zod.ts` @@ -30,7 +46,7 @@ const result = FlowNode.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Node unique ID | -| **type** | `Enum<'start' \| 'end' \| 'decision' \| 'assignment' \| 'loop' \| 'create_record' \| 'update_record' \| 'delete_record' \| 'get_record' \| 'http_request' \| 'script' \| 'screen' \| 'wait' \| 'subflow' \| 'connector_action' \| 'parallel_gateway' \| 'join_gateway' \| 'boundary_event'>` | ✅ | Action type | +| **type** | `string` | ✅ | Action type — a built-in FlowNodeAction id or a plugin-registered node type. Validated against the live action registry at registerFlow() (ADR-0018), not by a closed enum. | | **label** | `string` | ✅ | Node label | | **config** | `Record` | optional | Node configuration | | **connectorConfig** | `Object` | optional | | diff --git a/content/docs/references/automation/index.mdx b/content/docs/references/automation/index.mdx index 59e47002fe..90ff0cf051 100644 --- a/content/docs/references/automation/index.mdx +++ b/content/docs/references/automation/index.mdx @@ -16,5 +16,4 @@ This section contains all protocol schemas for the automation layer of ObjectSta - diff --git a/content/docs/references/automation/meta.json b/content/docs/references/automation/meta.json index 7032fecce7..41d03c7631 100644 --- a/content/docs/references/automation/meta.json +++ b/content/docs/references/automation/meta.json @@ -12,7 +12,6 @@ "state-machine", "sync", "trigger-registry", - "webhook", - "workflow" + "webhook" ] } \ No newline at end of file diff --git a/content/docs/references/automation/node-executor.mdx b/content/docs/references/automation/node-executor.mdx index 7f5ed0a44a..f85a107ae2 100644 --- a/content/docs/references/automation/node-executor.mdx +++ b/content/docs/references/automation/node-executor.mdx @@ -30,13 +30,70 @@ The protocol covers: ## TypeScript Usage ```typescript -import { NodeExecutorDescriptor, WaitEventType, WaitExecutorConfig, WaitResumePayload, WaitTimeoutBehavior } from '@objectstack/spec/automation'; -import type { NodeExecutorDescriptor, WaitEventType, WaitExecutorConfig, WaitResumePayload, WaitTimeoutBehavior } from '@objectstack/spec/automation'; +import { ActionCategory, ActionDescriptor, ActionParadigm, NodeExecutorDescriptor, WaitEventType, WaitExecutorConfig, WaitResumePayload, WaitTimeoutBehavior } from '@objectstack/spec/automation'; +import type { ActionCategory, ActionDescriptor, ActionParadigm, NodeExecutorDescriptor, WaitEventType, WaitExecutorConfig, WaitResumePayload, WaitTimeoutBehavior } from '@objectstack/spec/automation'; // Validate data -const result = NodeExecutorDescriptor.parse(data); +const result = ActionCategory.parse(data); ``` +--- + +## ActionCategory + +Action palette category + +### Allowed Values + +* `logic` +* `data` +* `io` +* `human` +* `control` +* `custom` + + +--- + +## ActionDescriptor + +Canonical cross-paradigm action/node descriptor (ADR-0018) + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Registry action/node type (matches the executor type) | +| **version** | `string` | ✅ | Executor version (semver) | +| **name** | `string` | ✅ | Display label (or i18n key) | +| **description** | `string` | optional | Action description | +| **icon** | `string` | optional | Icon id resolved by the designer | +| **category** | `Enum<'logic' \| 'data' \| 'io' \| 'human' \| 'control' \| 'custom'>` | ✅ | Palette category | +| **paradigms** | `Enum<'flow' \| 'workflow_rule' \| 'approval'>[]` | ✅ | Authoring surfaces that may offer this action | +| **configSchema** | `any` | optional | JSON Schema for the node config (drives form + parse validation) | +| **supportsPause** | `boolean` | ✅ | Supports async pause/resume | +| **supportsCancellation** | `boolean` | ✅ | Supports cancellation | +| **supportsRetry** | `boolean` | ✅ | Supports retry on failure | +| **needsOutbox** | `boolean` | ✅ | Dispatch via service-messaging outbox (retry/idempotency/dead-letter) | +| **isAsync** | `boolean` | ✅ | Suspends the flow awaiting an external reply | +| **source** | `Enum<'builtin' \| 'plugin'>` | ✅ | builtin = platform baseline; plugin = third-party contributed | +| **deprecated** | `boolean` | ✅ | Deprecated alias kept for back-compat | +| **aliasOf** | `string` | optional | Canonical type this alias forwards to | + + +--- + +## ActionParadigm + +Authoring paradigm that may offer this action + +### Allowed Values + +* `flow` +* `workflow_rule` +* `approval` + + --- ## NodeExecutorDescriptor diff --git a/content/docs/references/automation/workflow.mdx b/content/docs/references/automation/workflow.mdx deleted file mode 100644 index 6922044f30..0000000000 --- a/content/docs/references/automation/workflow.mdx +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: Workflow -description: Workflow protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs go in content/docs/guides/. */} - -Trigger events for workflow automation - - -**Source:** `packages/spec/src/automation/workflow.zod.ts` - - -## TypeScript Usage - -```typescript -import { ConnectorActionRef, CustomScriptAction, EmailAlertAction, FieldUpdateAction, HttpCallAction, PushNotificationAction, WorkflowTriggerType } from '@objectstack/spec/automation'; -import type { ConnectorActionRef, CustomScriptAction, EmailAlertAction, FieldUpdateAction, HttpCallAction, PushNotificationAction, WorkflowTriggerType } from '@objectstack/spec/automation'; - -// Validate data -const result = ConnectorActionRef.parse(data); -``` - ---- - -## ConnectorActionRef - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Action name | -| **type** | `string` | ✅ | | -| **connectorId** | `string` | ✅ | Target Connector ID (e.g. slack, twilio) | -| **actionId** | `string` | ✅ | Target Action ID (e.g. send_message) | -| **input** | `Record` | ✅ | Input parameters matching the action schema | - - ---- - -## CustomScriptAction - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Action name | -| **type** | `string` | ✅ | | -| **language** | `Enum<'javascript' \| 'typescript' \| 'python'>` | ✅ | Script language | -| **code** | `string` | ✅ | Script code to execute | -| **timeout** | `number` | ✅ | Execution timeout in milliseconds | -| **context** | `Record` | optional | Additional context variables | - - ---- - -## EmailAlertAction - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Action name | -| **type** | `string` | ✅ | | -| **template** | `string` | ✅ | Email template ID/DevName | -| **recipients** | `string[]` | ✅ | List of recipient emails or user IDs | - - ---- - -## FieldUpdateAction - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Action name | -| **type** | `string` | ✅ | | -| **field** | `string` | ✅ | Field to update | -| **value** | `any` | ✅ | Value or Formula to set | - - ---- - -## HttpCallAction - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Action name | -| **type** | `string` | ✅ | | -| **url** | `string` | ✅ | Target URL | -| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH'>` | ✅ | HTTP Method | -| **headers** | `Record` | optional | HTTP Headers | -| **body** | `string` | optional | Request body (JSON or text) | - - ---- - -## PushNotificationAction - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Action name | -| **type** | `string` | ✅ | | -| **title** | `string` | ✅ | Notification title | -| **body** | `string` | ✅ | Notification body text | -| **recipients** | `string[]` | ✅ | User IDs or device tokens | -| **data** | `Record` | optional | Additional data payload | -| **badge** | `number` | optional | Badge count (iOS) | -| **sound** | `string` | optional | Notification sound | -| **clickAction** | `string` | optional | Action/URL when notification is clicked | - - ---- - -## WorkflowTriggerType - -### Allowed Values - -* `on_create` -* `on_update` -* `on_create_or_update` -* `on_delete` -* `schedule` - - ---- - diff --git a/content/docs/references/data/datasource.mdx b/content/docs/references/data/datasource.mdx index 94bc992880..46dbdacd32 100644 --- a/content/docs/references/data/datasource.mdx +++ b/content/docs/references/data/datasource.mdx @@ -45,6 +45,7 @@ const result = Datasource.parse(data); | **active** | `boolean` | ✅ | Is datasource enabled | | **schemaMode** | `Enum<'managed' \| 'external' \| 'validate-only'>` | ✅ | Schema ownership mode | | **external** | `Object` | optional | External datasource federation settings (schemaMode != "managed") | +| **origin** | `Enum<'code' \| 'runtime'>` | ✅ | Datasource provenance (server-managed, read-only) | --- diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index 19a3786d47..7af95bcdc6 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -101,6 +101,7 @@ const result = Address.parse(data); * `url` * `phone` * `password` +* `secret` * `markdown` * `html` * `richtext` diff --git a/content/docs/references/kernel/metadata-plugin.mdx b/content/docs/references/kernel/metadata-plugin.mdx index 73edf30e53..589b89095d 100644 --- a/content/docs/references/kernel/metadata-plugin.mdx +++ b/content/docs/references/kernel/metadata-plugin.mdx @@ -72,8 +72,8 @@ cohesive plugin that "takes over" the entire platform's metadata management: ## TypeScript Usage ```typescript -import { MetadataBulkRegisterRequest, MetadataBulkResult, MetadataDependency, MetadataEvent, MetadataPluginConfig, MetadataPluginManifest, MetadataQuery, MetadataQueryResult, MetadataType, MetadataTypeRegistryEntry, MetadataValidationResult } from '@objectstack/spec/kernel'; -import type { MetadataBulkRegisterRequest, MetadataBulkResult, MetadataDependency, MetadataEvent, MetadataPluginConfig, MetadataPluginManifest, MetadataQuery, MetadataQueryResult, MetadataType, MetadataTypeRegistryEntry, MetadataValidationResult } from '@objectstack/spec/kernel'; +import { MetadataBulkRegisterRequest, MetadataBulkResult, MetadataDependency, MetadataEvent, MetadataQuery, MetadataQueryResult, MetadataType, MetadataValidationResult } from '@objectstack/spec/kernel'; +import type { MetadataBulkRegisterRequest, MetadataBulkResult, MetadataDependency, MetadataEvent, MetadataQuery, MetadataQueryResult, MetadataType, MetadataValidationResult } from '@objectstack/spec/kernel'; // Validate data const result = MetadataBulkRegisterRequest.parse(data); @@ -139,42 +139,6 @@ const result = MetadataBulkRegisterRequest.parse(data); | **payload** | `Record` | optional | Event-specific payload | ---- - -## MetadataPluginConfig - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **storage** | `Object` | ✅ | Storage backend configuration | -| **customizationPolicies** | `Object[]` | optional | Default customization policies per type | -| **mergeStrategy** | `Object` | optional | Merge strategy for package upgrades | -| **additionalTypes** | `Object[]` | optional | Additional custom metadata types | -| **enableEvents** | `boolean` | ✅ | Emit metadata change events | -| **validateOnWrite** | `boolean` | ✅ | Validate metadata on write | -| **enableVersioning** | `boolean` | ✅ | Track metadata version history | -| **cacheMaxItems** | `integer` | ✅ | Max items in memory cache | -| **bootstrap** | `Enum<'eager' \| 'lazy' \| 'artifact-only'>` | ✅ | How metadata is primed at plugin start (eager / lazy / artifact-only) | - - ---- - -## MetadataPluginManifest - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **id** | `string` | ✅ | Metadata plugin ID | -| **name** | `string` | ✅ | Plugin name | -| **version** | `string` | ✅ | Plugin version | -| **type** | `string` | ✅ | Plugin type | -| **description** | `string` | ✅ | Plugin description | -| **capabilities** | `Object` | ✅ | Plugin capabilities | -| **config** | `Object` | optional | Plugin configuration | - - --- ## MetadataQuery @@ -246,27 +210,6 @@ const result = MetadataBulkRegisterRequest.parse(data); * `skill` ---- - -## MetadataTypeRegistryEntry - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type identifier | -| **label** | `string` | ✅ | Display label for the metadata type | -| **description** | `string` | optional | Description of the metadata type | -| **filePatterns** | `string[]` | ✅ | Glob patterns to discover files of this type | -| **supportsOverlay** | `boolean` | ✅ | Whether overlay customization is supported | -| **allowOrgOverride** | `boolean` | ✅ | Allow per-org overlay writes via runtime metadata API | -| **allowRuntimeCreate** | `boolean` | ✅ | Allow runtime creation via API | -| **supportsVersioning** | `boolean` | ✅ | Whether version history is tracked | -| **executionPinned** | `boolean` | ✅ | Transaction rows reference a specific version_hash; history GC is disabled and getByHash() MUST resolve old hashes (ADR-0009) | -| **loadOrder** | `integer` | ✅ | Loading priority (lower = earlier) | -| **domain** | `Enum<'data' \| 'ui' \| 'automation' \| 'system' \| 'security' \| 'ai'>` | ✅ | Protocol domain | - - --- ## MetadataValidationResult diff --git a/content/docs/references/ui/action.mdx b/content/docs/references/ui/action.mdx index 1fcadd092c..b77cb0f9c4 100644 --- a/content/docs/references/ui/action.mdx +++ b/content/docs/references/ui/action.mdx @@ -82,7 +82,7 @@ const result = ActionLocation.parse(data); | **field** | `string` | optional | Snake case identifier (lowercase with underscores only) | | **objectOverride** | `string` | optional | Snake case identifier (lowercase with underscores only) | | **label** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | -| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| 'datetime' \| 'time' \| 'boolean' \| 'toggle' \| 'select' \| 'multiselect' \| 'radio' \| 'checkboxes' \| 'lookup' \| 'master_detail' \| 'tree' \| 'image' \| 'file' \| 'avatar' \| 'video' \| 'audio' \| 'formula' \| 'summary' \| 'autonumber' \| 'composite' \| 'repeater' \| 'record' \| 'location' \| 'address' \| 'code' \| 'json' \| 'color' \| 'rating' \| 'slider' \| 'signature' \| 'qrcode' \| 'progress' \| 'tags' \| 'vector'>` | optional | | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| 'datetime' \| 'time' \| 'boolean' \| 'toggle' \| 'select' \| 'multiselect' \| 'radio' \| 'checkboxes' \| 'lookup' \| 'master_detail' \| 'tree' \| 'image' \| 'file' \| 'avatar' \| 'video' \| 'audio' \| 'formula' \| 'summary' \| 'autonumber' \| 'composite' \| 'repeater' \| 'record' \| 'location' \| 'address' \| 'code' \| 'json' \| 'color' \| 'rating' \| 'slider' \| 'signature' \| 'qrcode' \| 'progress' \| 'tags' \| 'vector'>` | optional | | | **required** | `boolean` | ✅ | | | **options** | `Object[]` | optional | | | **placeholder** | `string` | optional | | diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index 24699dd7b0..09434b63c3 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -61,7 +61,7 @@ parallel. Flows are the primary automation building block in ObjectStack. ### Flow Node Types -Flows are built from **18 node types**: +Flows are built from **19 node types**: #### Control Flow @@ -96,6 +96,12 @@ Flows are built from **18 node types**: | `script` | Execute custom JavaScript/TypeScript logic | | `screen` | Display a UI form to the user (screen flows only) | +#### Human Decision + +| Node | Purpose | +|:-----|:--------| +| `approval` | Route a record for human sign-off — **suspends** the run until a decision, then continues down the `approve` / `reject` branch (contributed by `plugin-approvals`) | + ### Flow Variables Every flow defines input/output variables: @@ -219,70 +225,96 @@ Transitions can have conditions that must be met: } ``` -### Approval Processes +### Approvals (Flow Nodes) -An **ApprovalProcess** is the canonical multi-step review pattern for a -business object. Build with the `ApprovalProcess.create({...})` factory from -`@objectstack/spec/automation` and add to `defineStack({ approvals: [...] })`. +Since **ADR-0019** there is no standalone approval-process type. An approval is +authored as an **Approval node** (`type: 'approval'`) on an ordinary flow — the +run **suspends** when it reaches the node and **resumes** down the node's +`approve` / `reject` out-edge once a decision is recorded. Multi-step review is +just successive Approval nodes wired together on the canvas, so the whole review +is one diagram a reviewer (or AI) can read end-to-end. -```typescript -import { ApprovalProcess } from '@objectstack/spec/automation'; +> The old process-level concepts re-home onto the flow graph + node config — see +> the re-home table below. The approval *state* (`sys_approval_request` / +> `sys_approval_action`, the record lock, the status mirror, approver +> resolution) is unchanged and still owned by `plugin-approvals`. -export const OpportunityDiscountApproval = ApprovalProcess.create({ +```typescript +// A record-triggered flow: high-value opportunities need manager sign-off, +// and director sign-off too when the amount clears 500k. +{ name: 'opportunity_discount_approval', label: 'Opportunity Discount Approval', - object: 'opportunity', - active: true, - description: 'High-value opportunities (> 100k) require manager + director sign-off.', - - // Auto-submit + record locking — Phase B autopilot - entryCriteria: P`record.amount > 100000`, // CEL predicate - lockRecord: true, - approvalStatusField: 'approval_status', // mirrors pending|approved|rejected|recalled - - // Process-level hooks (run once per request) - onSubmit: [ - { type: 'inbox_notify', name: 'notify_approvers', - config: { to: 'pending_approvers', title: 'Discount approval needed', - body: 'Opportunity {record_id} (> 100k) is awaiting review.', link: '/system/approvals' } }, - ], - onFinalApprove: [ - { type: 'field_update', name: 'mark_won', - config: { field: 'stage', value: 'closed_won' } }, - { type: 'inbox_notify', name: 'notify_submitter_approved', - config: { to: 'submitter', title: 'Discount approved', - body: 'Your discount request on {record_id} was approved.', link: '/system/approvals' } }, - ], - onFinalReject: [ - { type: 'inbox_notify', name: 'notify_submitter_rejected', - config: { to: 'submitter', title: 'Discount rejected', - body: 'Rejected: {comment}', link: '/system/approvals' } }, - ], - onRecall: [ - { type: 'inbox_notify', name: 'notify_recall', - config: { to: 'pending_approvers', title: 'Discount request recalled', - body: 'Submitter recalled the request on {record_id}.' } }, - ], - - steps: [ + type: 'record_triggered', + trigger: { object: 'opportunity', event: 'after_update' }, + nodes: [ + { id: 'start', type: 'start' }, { - name: 'manager_review', + id: 'manager_review', + type: 'approval', label: 'Sales Manager Review', - approvers: [{ type: 'role', value: 'sales_manager' }], - behavior: 'first_response', // or 'unanimous' - rejectionBehavior: 'back_to_previous', // or 'reject_process' + config: { + approvers: [{ type: 'role', value: 'sales_manager' }], + behavior: 'first_response', // or 'unanimous' + lockRecord: true, // lock the record while pending + approvalStatusField: 'approval_status', // mirror pending|approved|rejected|recalled onto the row + }, }, + { id: 'needs_director', type: 'decision', config: { condition: cel`record.amount > 500000` } }, { - name: 'director_signoff', + id: 'director_signoff', + type: 'approval', label: 'Sales Director Sign-off', - approvers: [{ type: 'role', value: 'sales_director' }], - behavior: 'first_response', - rejectionBehavior: 'back_to_previous', + config: { + approvers: [{ type: 'role', value: 'sales_director' }], + behavior: 'unanimous', + approvalStatusField: 'approval_status', + }, }, + { id: 'mark_won', type: 'update_record', + config: { object: 'opportunity', recordId: '$record.id', values: { stage: 'closed_won' } } }, + { id: 'approved', type: 'end' }, + { id: 'rejected', type: 'end' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'manager_review', + // entry criteria re-homes onto the edge entering the approval node: + condition: cel`record.amount > 100000` }, + { id: 'e2', source: 'manager_review', target: 'needs_director', label: 'approve' }, + { id: 'e3', source: 'manager_review', target: 'rejected', label: 'reject' }, + { id: 'e4', source: 'needs_director', target: 'director_signoff', label: 'true' }, + { id: 'e5', source: 'needs_director', target: 'mark_won', label: 'false' }, + { id: 'e6', source: 'director_signoff', target: 'mark_won', label: 'approve' }, + { id: 'e7', source: 'director_signoff', target: 'rejected', label: 'reject' }, + { id: 'e8', source: 'mark_won', target: 'approved' }, ], -}); +} ``` +### Re-homing the old process model + +If you've seen the pre-ADR-0019 `ApprovalProcess.create({...})` shape, every +concept maps onto the flow: + +| Old process concept | Now | +|:--------------------|:----| +| `steps: [...]` (linear list) | successive **Approval nodes** joined by edges | +| `entryCriteria` (process or step) | a `condition` on the **edge entering** the node | +| `onApprove` / `onReject` actions | downstream **nodes** wired to the `approve` / `reject` out-edge | +| `rejectionBehavior: 'back_to_previous'` | a **back-edge** to an earlier node | +| `rejectionBehavior: 'reject_process'` | the `reject` edge routed to an `end` node | +| `approvers` / `behavior` / `lockRecord` / `approvalStatusField` / `escalation` | the Approval node's `config` (`ApprovalNodeConfigSchema`) | + +There is no `approvals: [...]` stack collection anymore — approval flows live in +your normal `flows: [...]`. + +### Recording a decision + +A decision is recorded through `ApprovalService.decide()` (or the REST routes +`POST /api/v1/approvals/requests/:id/approve` | `/reject`). That finalizes the +`sys_approval_request` and **resumes** the suspended run down the matching +branch — you never resume the flow by hand. + ### Approver Types | `type` | Resolves to | @@ -295,55 +327,41 @@ export const OpportunityDiscountApproval = ApprovalProcess.create({ | `field` | User id read from a record field (`value` = field name) | | `queue` | A data-ownership queue | -### Step Behavior - -- `behavior: 'first_response'` — the first approver to respond decides the step. -- `behavior: 'unanimous'` — every approver must approve. -- `rejectionBehavior: 'reject_process'` — rejection terminates the whole process. -- `rejectionBehavior: 'back_to_previous'` — rejection rolls back one step so - the submitter (or the previous approver) can revise. - -### Step & Process Actions - -Action types accepted in `onSubmit`, `onFinalApprove`, `onFinalReject`, -`onRecall`, and per-step `onApprove`/`onReject`: +### Node Config (`ApprovalNodeConfigSchema`) -| `type` | Purpose | -|:-------|:--------| -| `field_update` | Write a field on the target record | -| `inbox_notify` | Insert a `sys_notification` row (in-app inbox) | -| `email_alert` | Send templated email to recipients | -| `webhook` | POST JSON payload to an external URL | -| `script` | Run an L2 hook body (sandboxed JS) | -| `connector_action` | Invoke a Zapier-style connector action | +| Field | Purpose | +|:------|:--------| +| `approvers` | Who may act (≥ 1 — see Approver Types above) | +| `behavior` | `first_response` (first approver decides) or `unanimous` (all must approve). Default `first_response` | +| `lockRecord` | Lock the triggering record from edits while pending. Default `true` | +| `approvalStatusField` | Business-object field to mirror `pending`/`approved`/`rejected`/`recalled` onto (should be readonly) | +| `escalation` | Optional per-node SLA — `{ enabled, timeoutHours, action: reassign\|auto_approve\|auto_reject\|notify, escalateTo?, notifySubmitter }` | -### Per-Step Entry Criteria +### Branching, side-effects & rejection -Use `entryCriteria` on a step to make it conditional (e.g. only require -director sign-off for amounts > 500k): +These are wired on the **graph**, not in node config: -```typescript -{ - name: 'director_signoff', - entryCriteria: P`record.amount > 500000`, - approvers: [{ type: 'role', value: 'sales_director' }], - behavior: 'first_response', - rejectionBehavior: 'back_to_previous', -} -``` +- **Conditional step** — put a `decision` node before the Approval node, or a + `condition` on the edge entering it (the old per-step `entryCriteria`). +- **On approve / on reject** — wire downstream nodes (`update_record`, + `http_request`, an email node, …) to the `approve` / `reject` out-edge. +- **Roll back on reject** — route the `reject` edge as a **back-edge** to an + earlier node so the submitter can revise (the old `back_to_previous`). +- **Hard reject** — route the `reject` edge to an `end` node (the old + `reject_process`). ### Approval Best Practices -1. **Always set `entryCriteria` at the process level** so the runtime can - auto-submit on insert/update. Hand-crafted submission flows are a smell. +1. **Gate entry on the edge** (`condition` into the Approval node) so the flow + only pauses for records that actually need sign-off. 2. **Set `approvalStatusField`** to mirror status onto the row — views and formulas can then filter on it without joining `sys_approval_request`. 3. **Keep `lockRecord: true`** unless you have a strong reason to allow edits while pending — otherwise approvers chase a moving target. -4. **Prefer `back_to_previous`** for rejection unless the request is truly - terminal; it lets submitters iterate without re-opening the process. -5. **Use `inbox_notify` over `email_alert`** for internal approval - notifications — it keeps the trail inside ObjectStack's audit log. +4. **Model rejection as a visible branch** — a back-edge to revise, or an `end` + node to terminate. The path is on the diagram, not hidden in config. +5. **Notify from downstream nodes** wired to the `approve` / `reject` edges + rather than expecting the node to send mail itself. --- @@ -439,7 +457,7 @@ For enterprise automation design, align with this CRM-style structure: | Automation Type | Typical Location | Pattern | |:--|:--|:--| | Screen flow | `src/flows/*.flow.ts` | Use explicit `variables`, node graph (`nodes` + `edges`), and decision branches | -| Approval process | `src/approvals/*.approval.ts` | Set `entryCriteria`, `lockRecord`, `approvalStatusField`, and stage-level approvers | +| Approval flow | `src/flows/*.flow.ts` | A flow with `approval` node(s); set `approvers` / `behavior` / `lockRecord` / `approvalStatusField` in node `config`, branch on `approve` / `reject` edges | | Flow registry | `src/flows/index.ts` | Export `allFlows: Flow[]` and register centrally in `defineStack({ flows })` | | Action-to-flow bridge | `src/actions/*.actions.ts` | Trigger screen flows via `Action.type = 'flow'` for user-driven automation entry |