diff --git a/.changeset/retire-workflow-rule-paradigm.md b/.changeset/retire-workflow-rule-paradigm.md new file mode 100644 index 000000000..47d9505d9 --- /dev/null +++ b/.changeset/retire-workflow-rule-paradigm.md @@ -0,0 +1,17 @@ +--- +'@objectstack/spec': patch +'@objectstack/service-automation': patch +--- + +chore(automation): retire the `workflow_rule` authoring paradigm (ADR-0018 M5 dropped) + +ADR-0019 already removed the Workflow-Rule → Flow compiler (Workflow Rules were +removed in #1398 and `workflow` was reclaimed for state machines), but the +`workflow_rule` paradigm tag survived in `ActionParadigmSchema` and on every +built-in node descriptor. There is no declarative Workflow-Rule authoring view +to feed, so the tag is now retired: `ActionParadigmSchema` keeps `['flow', +'approval']`, and the `http` / `notify` / `connector_action` descriptors (plus +the deprecated-alias fallback) advertise `['flow', 'approval']`. Approval +execution convergence is delivered by the ADR-0019 approval Flow node, not a +compiler. ADR-0018's status and migration table are updated to mark M3 shipped, +M4 framework-complete, and M5 dropped. diff --git a/docs/adr/0018-unified-node-action-registry.md b/docs/adr/0018-unified-node-action-registry.md index f79956916..6b229cfd5 100644 --- a/docs/adr/0018-unified-node-action-registry.md +++ b/docs/adr/0018-unified-node-action-registry.md @@ -1,6 +1,6 @@ # ADR-0018: Unified Node/Action Registry across Flow, Workflow-Rule & Approval -**Status**: Accepted (2026-05-31) — M1 + M2 implemented (built-in nodes folded into the core plugin; descriptor API + `GET /automation/actions` shipped). M3 (outbox-backed `http`/`notify`) next. +**Status**: Accepted (2026-05-31) — M1–M3 implemented; M4 framework-side complete (designer config-forms tracked in `../objectui`); M5 **dropped** (Workflow-Rule→Flow compiler removed per [ADR-0019](./0019-approval-as-flow-node.md); approval execution converged via the approval Flow node, not a compiler). The `workflow_rule` authoring paradigm is retired. **Deciders**: ObjectStack Protocol Architects **Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (one Zod source of truth per metadata type), [ADR-0012](./0012-notification-platform.md) (generalized outbox in `service-messaging`) **Consumers**: `@objectstack/spec` (`automation/`), `@objectstack/services/service-automation`, `@objectstack/plugins/plugin-approvals`, `@objectstack/plugins/plugin-webhooks` → `service-messaging`, every plugin that registers a node executor, `../objectui` (`plugin-workflow` designer) @@ -168,9 +168,9 @@ No fourth engine. Workflow Rules stays a **simplified authoring view** for busin |:---|:---|:---| | M1 ✅ | Add canonical `ActionDescriptorSchema` (+ `defineActionDescriptor`) alongside the legacy `NodeExecutorDescriptor`; `FlowNodeSchema.type` → validated `string` (with `FlowNodeAction`/`FLOW_BUILTIN_NODE_TYPES` retained as the seed set); `registerFlow()` soft-validates node types against the live registry (warn, don't hard-fail). | **Shipped.** Existing flows keep validating (built-in types seed-registered); plugin-registered node types are now legal flow nodes. | | M2 (partial ✅) | Built-in nodes (logic/crud/http/screen) publish descriptors and are **folded into the core `AutomationServicePlugin`** (seeded via `installBuiltinNodes()`), so `automation` is a self-contained capability — no companion node-pack plugins, no `extras` in the capability loader. `connector_action` dropped from the baseline (an integration concern needing a connector registry the platform doesn't ship; left to the integration layer / marketplace plugins via the still-open `registerNodeExecutor()`). Descriptors tagged `source: 'builtin'` vs `'plugin'`. `AutomationEngine.getActionDescriptors()`/`getActionDescriptor()` + optional `IAutomationService.getActionDescriptors()`. `GET /api/v1/automation/actions` shipped via `HttpDispatcher.handleAutomation` (`?paradigm`/`?source`/`?category` filters; `AutomationActionsResponseSchema` in spec; declarative entry in `DEFAULT_AUTOMATION_ROUTES`). | Additive; built-in flows keep working from the core plugin alone. Hosts that hand-assembled the four `*NodesPlugin` classes drop them (the classes are gone). | -| M3 | Introduce `http` + `notify` executors backed by `service-messaging`; register `http_request`/`http_call`/`webhook` as deprecated aliases | Old node types keep running via alias. | -| M4 | Designer palette + config forms driven by the registry; remove hardcoded `FlowNodeType` | Designer-only; old saved graphs still load. | -| M5 | Workflow-Rule → Flow compiler; `plugin-approvals` action executor delegates to the shared `http`/`notify` executors | Approval/Workflow-Rule definitions unchanged; execution path converges. | +| M3 ✅ | Canonical `http` + `notify` executors backed by `service-messaging` (HTTP outbox `sys_http_delivery` / notification outbox); `http_request`/`http_call`/`webhook` registered as **deprecated aliases** (`registerNodeAlias`, `needsOutbox`). | **Shipped.** Old node types keep running via alias; degrade to inline when no outbox is wired. | +| M4 (framework ✅) | Hardcoded `FlowNodeType` removed (M1 turned `FlowNodeSchema.type` into a validated string seeded from `FLOW_BUILTIN_NODE_TYPES`); `GET /automation/actions` drives the palette. Remaining: designer config-forms rendered from `descriptor.configSchema` — tracked in `../objectui`. | Framework carries no closed node enum; old saved graphs still load. | +| ~~M5~~ **Dropped** | Workflow-Rule → Flow compiler **removed** — there is no declarative Workflow-Rule authoring type to compile (Workflow Rules removed in #1398; `workflow` reclaimed for state machines, ADR-0020). The `workflow_rule` paradigm tag is retired from `ActionParadigmSchema` and all descriptors. Approval execution convergence is delivered by [ADR-0019](./0019-approval-as-flow-node.md) (approval is a durable-pause Flow node; side-effects run on the shared `http`/`notify` executors), not a compiler. | No legacy to migrate (greenfield). | --- @@ -231,6 +231,6 @@ Managed credentials/secret vault, OAuth2 token refresh, multi-tenant connection ### Implementation checklist - [x] `AutomationEngine`: connector registry (`registerConnector` / `unregisterConnector` / `resolveConnectorAction` / `getRegisteredConnectors`) + `ConnectorActionHandler` / `ConnectorActionContext` types. -- [x] `builtin/connector-nodes.ts`: `connector_action` executor + descriptor (`category: 'io'`, `source: 'builtin'`, `paradigms: ['flow','workflow_rule','approval']`), wired into `installBuiltinNodes()`. The core plugin now seeds 11 baseline node types (was 10). +- [x] `builtin/connector-nodes.ts`: `connector_action` executor + descriptor (`category: 'io'`, `source: 'builtin'`, `paradigms: ['flow','approval']`), wired into `installBuiltinNodes()`. The core plugin now seeds 11 baseline node types (was 10). - [x] First concrete plugin `@objectstack/connector-rest` (the reference connector) validating the registry — `request` action, static auth (`none`/`api-key`/`basic`/`bearer`), no OAuth2 refresh. - [x] Tests: baseline dispatch (fake connector) + REST plugin auth-header injection + end-to-end kernel boot (both plugins → `connector_action` flow → REST handler). diff --git a/packages/runtime/src/http-dispatcher.test.ts b/packages/runtime/src/http-dispatcher.test.ts index 97a74323d..b141b2e48 100644 --- a/packages/runtime/src/http-dispatcher.test.ts +++ b/packages/runtime/src/http-dispatcher.test.ts @@ -147,7 +147,7 @@ describe('HttpDispatcher', () => { trigger: vi.fn().mockResolvedValue({ success: true }), getActionDescriptors: vi.fn().mockReturnValue([ { type: 'decision', name: 'Decision', category: 'logic', paradigms: ['flow'], source: 'builtin' }, - { type: 'http_request', name: 'HTTP Request', category: 'io', paradigms: ['flow', 'workflow_rule'], source: 'builtin' }, + { type: 'http_request', name: 'HTTP Request', category: 'io', paradigms: ['flow', 'approval'], source: 'builtin' }, { type: 'send_sms', name: 'Send SMS', category: 'io', paradigms: ['flow'], source: 'plugin' }, ]), getConnectorDescriptors: vi.fn().mockReturnValue([ @@ -270,7 +270,7 @@ describe('HttpDispatcher', () => { }); it('should filter GET /actions by ?paradigm', async () => { - const result = await dispatcher.handleAutomation('actions', 'GET', {}, { request: {} }, { paradigm: 'workflow_rule' }); + const result = await dispatcher.handleAutomation('actions', 'GET', {}, { request: {} }, { paradigm: 'approval' }); expect(result.handled).toBe(true); expect(result.response?.body?.data?.total).toBe(1); expect(result.response?.body?.data?.actions[0].type).toBe('http_request'); diff --git a/packages/services/service-automation/src/builtin/connector-nodes.test.ts b/packages/services/service-automation/src/builtin/connector-nodes.test.ts index 1ee7d249a..188366e1d 100644 --- a/packages/services/service-automation/src/builtin/connector-nodes.test.ts +++ b/packages/services/service-automation/src/builtin/connector-nodes.test.ts @@ -51,7 +51,7 @@ describe('connector_action (baseline node)', () => { expect(descriptor?.source).toBe('builtin'); expect(descriptor?.category).toBe('io'); expect(descriptor?.paradigms).toEqual( - expect.arrayContaining(['flow', 'workflow_rule', 'approval']), + expect.arrayContaining(['flow', 'approval']), ); }); diff --git a/packages/services/service-automation/src/builtin/connector-nodes.ts b/packages/services/service-automation/src/builtin/connector-nodes.ts index 3d3f9aee8..acb539a77 100644 --- a/packages/services/service-automation/src/builtin/connector-nodes.ts +++ b/packages/services/service-automation/src/builtin/connector-nodes.ts @@ -32,8 +32,9 @@ export function registerConnectorNodes(engine: AutomationEngine, ctx: PluginCont category: 'io', source: 'builtin', supportsRetry: true, - // Present in all three authoring paradigms (ADR-0018 §registry table). - paradigms: ['flow', 'workflow_rule', 'approval'], + // Present in both authoring paradigms (ADR-0018 §registry table; + // workflow_rule retired per ADR-0019). + paradigms: ['flow', 'approval'], // Config contract — drives the Studio property form and flow validation. configSchema: { type: 'object', diff --git a/packages/services/service-automation/src/builtin/http-nodes.test.ts b/packages/services/service-automation/src/builtin/http-nodes.test.ts index 24409e8de..69f273168 100644 --- a/packages/services/service-automation/src/builtin/http-nodes.test.ts +++ b/packages/services/service-automation/src/builtin/http-nodes.test.ts @@ -58,7 +58,7 @@ describe('http (canonical node) + deprecated aliases', () => { expect(d?.source).toBe('builtin'); expect(d?.category).toBe('io'); expect(d?.needsOutbox).toBe(true); - expect(d?.paradigms).toEqual(expect.arrayContaining(['flow', 'workflow_rule', 'approval'])); + expect(d?.paradigms).toEqual(expect.arrayContaining(['flow', 'approval'])); }); it('registers http_request/http_call/webhook as deprecated aliases of http', () => { diff --git a/packages/services/service-automation/src/builtin/http-nodes.ts b/packages/services/service-automation/src/builtin/http-nodes.ts index db01909a4..e99f19af3 100644 --- a/packages/services/service-automation/src/builtin/http-nodes.ts +++ b/packages/services/service-automation/src/builtin/http-nodes.ts @@ -73,7 +73,7 @@ export function registerHttpNodes(engine: AutomationEngine, ctx: PluginContext): // and the messaging HTTP outbox is wired). needsOutbox: true, supportsRetry: true, - paradigms: ['flow', 'workflow_rule', 'approval'], + paradigms: ['flow', 'approval'], configSchema: { type: 'object', required: ['url'], diff --git a/packages/services/service-automation/src/builtin/notify-node.test.ts b/packages/services/service-automation/src/builtin/notify-node.test.ts index 6f0bc820f..e04822ec3 100644 --- a/packages/services/service-automation/src/builtin/notify-node.test.ts +++ b/packages/services/service-automation/src/builtin/notify-node.test.ts @@ -72,7 +72,7 @@ describe('notify (baseline node)', () => { expect(descriptor?.source).toBe('builtin'); expect(descriptor?.category).toBe('io'); expect(descriptor?.paradigms).toEqual( - expect.arrayContaining(['flow', 'workflow_rule', 'approval']), + expect.arrayContaining(['flow', 'approval']), ); }); diff --git a/packages/services/service-automation/src/builtin/notify-node.ts b/packages/services/service-automation/src/builtin/notify-node.ts index 2d1d0a596..2f5aaee1b 100644 --- a/packages/services/service-automation/src/builtin/notify-node.ts +++ b/packages/services/service-automation/src/builtin/notify-node.ts @@ -67,7 +67,7 @@ export function registerNotifyNode(engine: AutomationEngine, ctx: PluginContext) // Delivery is outbox-backed inside the messaging service (ADR-0030 // emit → sys_notification_delivery), so it inherits retry/dead-letter. needsOutbox: true, - paradigms: ['flow', 'workflow_rule', 'approval'], + paradigms: ['flow', 'approval'], }), async execute(node, variables, context) { const cfg = (node.config ?? {}) as Record; diff --git a/packages/services/service-automation/src/engine.ts b/packages/services/service-automation/src/engine.ts index cc0a74db4..042fba881 100644 --- a/packages/services/service-automation/src/engine.ts +++ b/packages/services/service-automation/src/engine.ts @@ -450,7 +450,7 @@ export class AutomationEngine implements IAutomationService { description: `Deprecated alias of '${canonicalType}' (ADR-0018 M3). Author new flows with '${canonicalType}'.`, category: meta?.category ?? 'io', source: 'builtin', - paradigms: meta?.paradigms ?? ['flow', 'workflow_rule', 'approval'], + paradigms: meta?.paradigms ?? ['flow', 'approval'], supportsRetry: true, needsOutbox: meta?.needsOutbox ?? false, deprecated: true, diff --git a/packages/spec/src/automation/node-executor.zod.ts b/packages/spec/src/automation/node-executor.zod.ts index 19ecbf5b5..181b8578a 100644 --- a/packages/spec/src/automation/node-executor.zod.ts +++ b/packages/spec/src/automation/node-executor.zod.ts @@ -189,8 +189,10 @@ export type ActionCategory = z.infer; */ export const ActionParadigmSchema = lazySchema(() => z.enum([ 'flow', // visual Flow canvas - 'workflow_rule', // declarative Workflow Rule authoring view (compiles to Flow) 'approval', // Approval Process steps + // 'workflow_rule' retired (ADR-0018 M5 dropped; see ADR-0019): Workflow Rules + // were removed in #1398 and `workflow` was reclaimed for state machines, so + // there is no declarative rule authoring view to compile to Flow. ]).describe('Authoring paradigm that may offer this action')); export type ActionParadigm = z.infer;