Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/retire-workflow-rule-paradigm.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 5 additions & 5 deletions docs/adr/0018-unified-node-action-registry.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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). |

---

Expand Down Expand Up @@ -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).
4 changes: 2 additions & 2 deletions packages/runtime/src/http-dispatcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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']),
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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']),
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
Expand Down
2 changes: 1 addition & 1 deletion packages/services/service-automation/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion packages/spec/src/automation/node-executor.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,10 @@ export type ActionCategory = z.infer<typeof ActionCategorySchema>;
*/
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<typeof ActionParadigmSchema>;
Expand Down