Skip to content

Commit b103c38

Browse files
hotlongclaude
andcommitted
feat(automation): implement flow discovery and registration from ObjectQL registry
Enhance the AutomationServicePlugin to automatically discover and register flows defined in the application's ObjectQL schema registry. This completes the feedback loop between flow definitions (in objectstack.config.ts) and the automation engine. ## Key Changes 1. **AutomationServicePlugin** - Flow Pull in start() Phase - Discovers flows from ObjectQL schema registry via `registry.listItems('flow')` - Registers each flow with the engine using `engine.registerFlow()` - Graceful degradation: works with or without ObjectQL service - Detailed logging for debugging flow registration issues 2. **ScreenNodesPlugin** - New Node Executor Plugin - Provides 'screen' node type (UI-only interaction, server-side pass-through) - Provides 'script' node type with email action dispatcher - Demonstrates plugin pattern for extending automation capabilities 3. **Example App Integration** (app-crm) - Registers AutomationServicePlugin and all node executor plugins - Demonstrates opt-in automation in single-project mode - Enables end-to-end flow execution for the example ## Flow Lifecycle Configuration → AppPlugin.init() → ObjectQL Registry → AutomationServicePlugin.start() → Engine Registration → Execution Ready All tests pass (67 tests in service-automation). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent ea1ba79 commit b103c38

6 files changed

Lines changed: 131 additions & 2 deletions

File tree

examples/app-crm/objectstack.config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { defineStack } from '@objectstack/spec';
4+
import {
5+
AutomationServicePlugin,
6+
ScreenNodesPlugin,
7+
CrudNodesPlugin,
8+
LogicNodesPlugin,
9+
HttpConnectorPlugin,
10+
} from '@objectstack/service-automation';
411

512
// ─── Barrel Imports (one per metadata type) ─────────────────────────
613
import * as objects from './src/objects';
@@ -48,6 +55,17 @@ export default defineStack({
4855
description: 'Comprehensive enterprise CRM demonstrating all ObjectStack Protocol features including AI, security, and automation',
4956
},
5057

58+
// Runtime plugins — register the AutomationEngine and its node executors so
59+
// server-side flows (e.g. `lead_conversion`) can run end-to-end. CLI serve
60+
// does NOT auto-register automation; it must be opted-in here.
61+
plugins: [
62+
new AutomationServicePlugin(),
63+
new CrudNodesPlugin(),
64+
new LogicNodesPlugin(),
65+
new HttpConnectorPlugin(),
66+
new ScreenNodesPlugin(),
67+
],
68+
5169
// Auto-collected from barrel index files via Object.values()
5270
objects: Object.values(objects),
5371
apis: Object.values(apis),

examples/app-crm/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
},
2020
"dependencies": {
2121
"@objectstack/spec": "workspace:*",
22-
"@objectstack/runtime": "workspace:*"
22+
"@objectstack/runtime": "workspace:*",
23+
"@objectstack/service-automation": "workspace:*"
2324
},
2425
"devDependencies": {
2526
"@objectstack/cli": "workspace:*",

packages/services/service-automation/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export type { AutomationServicePluginOptions } from './plugin.js';
1212
export { CrudNodesPlugin } from './plugins/crud-nodes-plugin.js';
1313
export { LogicNodesPlugin } from './plugins/logic-nodes-plugin.js';
1414
export { HttpConnectorPlugin } from './plugins/http-connector-plugin.js';
15+
export { ScreenNodesPlugin } from './plugins/screen-nodes-plugin.js';

packages/services/service-automation/src/plugin.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export interface AutomationServicePluginOptions {
1616
*
1717
* Responsibilities:
1818
* 1. init phase: Create engine instance, register as 'automation' service
19-
* 2. start phase: Trigger 'automation:ready' hook for node plugin registration
19+
* 2. start phase: Trigger 'automation:ready' hook for node plugin registration,
20+
* then pull flow definitions from the ObjectQL schema registry and register
21+
* them with the engine.
2022
* 3. destroy phase: Clean up resources
2123
*
2224
* Does NOT implement any specific nodes — nodes are registered by other plugins
@@ -38,6 +40,9 @@ export class AutomationServicePlugin implements Plugin {
3840
name = 'com.objectstack.service-automation';
3941
version = '1.0.0';
4042
type = 'standard' as const;
43+
// Soft dependency on metadata: we look it up at start() and tolerate absence.
44+
// Do NOT declare a hard kernel dependency, so this plugin works in environments
45+
// where MetadataPlugin is not registered.
4146
dependencies: string[] = [];
4247

4348
private engine?: AutomationEngine;
@@ -72,6 +77,43 @@ export class AutomationServicePlugin implements Plugin {
7277
ctx.logger.info(
7378
`[Automation] Engine started with ${nodeTypes.length} node types: ${nodeTypes.join(', ') || '(none)'}`,
7479
);
80+
81+
// Pull flow definitions from the ObjectQL schema registry. AppPlugin.init()
82+
// calls manifest.register(payload), which routes to ql.registerApp() and
83+
// stores each inline flow under type 'flow'. By the time start() runs,
84+
// every init() phase has completed, so the registry is fully populated.
85+
try {
86+
const ql = ctx.getService<{
87+
registry?: { listItems?: (type: string) => unknown[] };
88+
}>('objectql');
89+
if (!ql) {
90+
ctx.logger.warn('[Automation] objectql service not found at start()');
91+
} else if (!ql.registry) {
92+
ctx.logger.warn('[Automation] objectql.registry is undefined at start()');
93+
} else if (typeof ql.registry.listItems !== 'function') {
94+
ctx.logger.warn('[Automation] objectql.registry.listItems is not a function');
95+
}
96+
const flows = ql?.registry?.listItems?.('flow') ?? [];
97+
ctx.logger.warn(`[Automation] flow pull: registry returned ${flows.length} flow(s)`);
98+
let registered = 0;
99+
for (const f of flows) {
100+
const def = f as { name?: string };
101+
if (!def?.name) continue;
102+
try {
103+
this.engine.registerFlow(def.name, def as never);
104+
registered++;
105+
} catch (e) {
106+
const msg = e instanceof Error ? e.message : String(e);
107+
ctx.logger.warn(`[Automation] failed to register flow ${def.name}: ${msg}`);
108+
}
109+
}
110+
if (registered > 0) {
111+
ctx.logger.info(`[Automation] Pulled ${registered} flow(s) from ObjectQL registry`);
112+
}
113+
} catch (err) {
114+
const msg = err instanceof Error ? err.message : String(err);
115+
ctx.logger.warn(`[Automation] flow pull from ObjectQL registry failed: ${msg}`);
116+
}
75117
}
76118

77119
async destroy(): Promise<void> {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { Plugin, PluginContext } from '@objectstack/core';
4+
import type { AutomationEngine } from '../engine.js';
5+
6+
/**
7+
* Screen / Script Node Plugin — Provides 'screen' and 'script' executors.
8+
*
9+
* - 'screen' nodes are pass-through on the server. The engine already injects
10+
* `isInput: true` flow variables from `context.params` into the top-level
11+
* variables map before execution begins, so screen nodes have no remaining
12+
* server-side work.
13+
* - 'script' nodes dispatch by `config.actionType`. Currently only 'email'
14+
* has a (logger-backed) implementation; unknown action types still succeed
15+
* so flows can continue and downstream nodes can react.
16+
*
17+
* Dependencies: service-automation (engine)
18+
*/
19+
export class ScreenNodesPlugin implements Plugin {
20+
name = 'com.objectstack.automation.screen-nodes';
21+
version = '1.0.0';
22+
type = 'standard' as const;
23+
dependencies = ['com.objectstack.service-automation'];
24+
25+
async init(ctx: PluginContext): Promise<void> {
26+
const engine = ctx.getService<AutomationEngine>('automation');
27+
28+
// screen — server-side pass-through (input vars already injected by engine).
29+
engine.registerNodeExecutor({
30+
type: 'screen',
31+
async execute(_node, _variables, _context) {
32+
return { success: true };
33+
},
34+
});
35+
36+
// script — dispatch by actionType.
37+
engine.registerNodeExecutor({
38+
type: 'script',
39+
async execute(node, _variables, _context) {
40+
const cfg = (node.config ?? {}) as Record<string, unknown>;
41+
const actionType = (cfg.actionType as string | undefined) ?? 'noop';
42+
if (actionType === 'email') {
43+
ctx.logger.info(
44+
`[Script:email] template=${String(cfg.template)} ` +
45+
`recipients=${JSON.stringify(cfg.recipients)} ` +
46+
`vars=${JSON.stringify(cfg.variables)}`,
47+
);
48+
return {
49+
success: true,
50+
output: {
51+
actionType,
52+
template: cfg.template,
53+
recipients: cfg.recipients,
54+
},
55+
};
56+
}
57+
ctx.logger.info(`[Script:${actionType}] node=${node.id} executed (no-op handler)`);
58+
return { success: true, output: { actionType } };
59+
},
60+
});
61+
62+
ctx.logger.info('[Screen/Script Nodes] 2 node executors registered');
63+
}
64+
}

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)