Skip to content

Commit a3bd4e5

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(showcase): wire the automation demo end-to-end (#1430)
Activates the showcase's automation chains now that the platform supports them (capability tokens from #1426; conditional/record-change flows fixed in #1429): - requires: ['ui','automation','approvals','messaging','triggers','job'] so the notify node delivers and record-change/schedule flows auto-fire. - Register the `rest` + `slack` connectors for the connector_action node. `rest` points at the running server (SHOWCASE_SELF_URL) so its call + response are observable with no external dependency. - Two worked flows: ScheduledDigestFlow (interval schedule → notify → inbox) and TaskCompletedRestPingFlow (record-change → rest connector GET /health). - Fix BudgetApprovalFlow's decision node: branch on edge `condition` (`budget > 500000` / `<= 500000`) per the flow spec, instead of the unevaluated node-level config + true/false labels — so budgets ≤ $500k correctly skip the executive approval step. - Ambient `process` decl so `pnpm typecheck` stays green without @types/node. Verified end-to-end in the browser: schedule digest, reassign→notify→inbox (email→user-id resolved), mark-done→REST/Slack/email, budget→approval with correct decision routing and approve→resume. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a6d4cbb commit a3bd4e5

4 files changed

Lines changed: 152 additions & 5 deletions

File tree

examples/app-showcase/objectstack.config.ts

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

33
import { defineStack } from '@objectstack/spec';
4+
import { ConnectorRestPlugin } from '@objectstack/connector-rest';
5+
import { ConnectorSlackPlugin } from '@objectstack/connector-slack';
46

57
import * as objects from './src/objects/index.js';
68
import { TaskViews, ProjectViews } from './src/views/index.js';
@@ -26,6 +28,11 @@ import { allDatasources } from './src/datasources/index.js';
2628
import { allPortals } from './src/portals/index.js';
2729
import { ShowcaseSeedData } from './src/data/index.js';
2830

31+
// Ambient `process` for the env-var overrides below — the showcase tsconfig
32+
// doesn't pull in `@types/node`, but the CLI provides the real `process` at
33+
// runtime. Keeps `pnpm typecheck` green without widening the type surface.
34+
declare const process: { env: Record<string, string | undefined> };
35+
2936
/**
3037
* Showcase — a kitchen-sink workspace that exercises every metadata type,
3138
* every view type, every chart type, and the major end-to-end capability
@@ -49,9 +56,35 @@ export default defineStack({
4956
description: 'Kitchen-sink workspace covering all metadata types, all view types, and the major capability chains.',
5057
},
5158

52-
// `approvals` loads ApprovalsServicePlugin so the `approval` flow node
53-
// (ADR-0019) is contributed to the engine — the showcase flows use it.
54-
requires: ['ui', 'automation', 'approvals'],
59+
// Capability tokens the CLI resolves to platform plugins:
60+
// • automation — AutomationServicePlugin (flow engine + node executors).
61+
// • approvals — ApprovalsServicePlugin, so the `approval` flow node
62+
// (ADR-0019) is contributed to the engine.
63+
// • messaging — MessagingServicePlugin, so the `notify` node delivers to
64+
// the inbox channel (`sys_inbox_message` rows) instead of
65+
// degrading to a logged no-op.
66+
// • triggers — record-change + schedule FlowTrigger plugins, so the
67+
// autolaunched / schedule flows below actually auto-fire.
68+
// • job — JobServicePlugin, the timing backend the schedule trigger
69+
// delegates to (interval / cron jobs).
70+
requires: ['ui', 'automation', 'approvals', 'messaging', 'triggers', 'job'],
71+
72+
// Concrete connectors for the `connector_action` node. The baseline engine
73+
// ships the dispatch node + an empty registry; these plugins populate it.
74+
// • rest → points at the running server itself, so the REST connector
75+
// flow's call + response are observable on the flow run with no
76+
// external dependency. Override the target with SHOWCASE_SELF_URL.
77+
// • slack → registered so TaskCompletedSlackFlow resolves its connector;
78+
// live posting needs a real bot token (set SLACK_BOT_TOKEN).
79+
plugins: [
80+
new ConnectorRestPlugin({
81+
name: 'rest',
82+
baseUrl: process.env.SHOWCASE_SELF_URL ?? 'http://127.0.0.1:3000',
83+
}),
84+
new ConnectorSlackPlugin({
85+
token: process.env.SLACK_BOT_TOKEN ?? 'xoxb-showcase-demo-token',
86+
}),
87+
],
5588

5689
// Infrastructure
5790
datasources: allDatasources,

examples/app-showcase/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
"verify": "pnpm typecheck && pnpm test"
2121
},
2222
"dependencies": {
23+
"@objectstack/connector-rest": "workspace:*",
24+
"@objectstack/connector-slack": "workspace:*",
2325
"@objectstack/runtime": "workspace:*",
2426
"@objectstack/spec": "workspace:*"
2527
},

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

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,12 @@ export const BudgetApprovalFlow = defineFlow({
197197
{ id: 'e1', source: 'start', target: 'manager_review' },
198198
{ id: 'e2', source: 'manager_review', target: 'needs_exec', label: 'approve' },
199199
{ id: 'e3', source: 'manager_review', target: 'rejected', label: 'reject' },
200-
{ id: 'e4', source: 'needs_exec', target: 'exec_review', label: 'true' },
201-
{ id: 'e5', source: 'needs_exec', target: 'approved', label: 'false' },
200+
// Decision branching is edge-condition driven (flow spec): the engine
201+
// routes a decision node by evaluating each out-edge's `condition`. Carry
202+
// the predicate on the edges (the node `config.condition` alone is not
203+
// evaluated by the engine), so budgets ≤ $500k skip the executive step.
204+
{ id: 'e4', source: 'needs_exec', target: 'exec_review', label: 'true', condition: 'budget > 500000' },
205+
{ id: 'e5', source: 'needs_exec', target: 'approved', label: 'false', condition: 'budget <= 500000' },
202206
{ id: 'e6', source: 'exec_review', target: 'approved', label: 'approve' },
203207
{ id: 'e7', source: 'exec_review', target: 'rejected', label: 'reject' },
204208
],
@@ -258,10 +262,112 @@ export const TaskCompletedSlackFlow = defineFlow({
258262
],
259263
});
260264

265+
/**
266+
* Scheduled Digest — the worked `schedule` trigger example.
267+
*
268+
* A `type: 'schedule'` flow whose start node carries an interval descriptor.
269+
* The automation engine parses that into a schedule binding; the schedule
270+
* trigger plugin (`@objectstack/plugin-trigger-schedule`, paired with the job
271+
* service) registers a job that fires this flow every interval. Each tick runs
272+
* the `notify` node, dropping a fresh `sys_inbox_message` row — so the
273+
* scheduled fire is observable end-to-end with no manual `engine.execute()`.
274+
*
275+
* Install `requires: ['automation', 'triggers', 'job', 'messaging']` and this
276+
* flow auto-launches on the interval.
277+
*/
278+
export const ScheduledDigestFlow = defineFlow({
279+
name: 'showcase_scheduled_digest',
280+
label: 'Scheduled Project Digest (interval)',
281+
description: 'Fires on a fixed interval and posts a digest to an inbox — demonstrates the schedule trigger.',
282+
type: 'schedule',
283+
nodes: [
284+
{
285+
id: 'start',
286+
type: 'start',
287+
label: 'Every 20s',
288+
config: {
289+
// Interval keeps the demo observable in-session; production digests
290+
// would use a cron expression, e.g. { type: 'cron', expression: '0 8 * * *' }.
291+
schedule: { type: 'interval', intervalMs: 20000 },
292+
},
293+
},
294+
{
295+
id: 'digest',
296+
type: 'notify',
297+
label: 'Post Digest to Inbox',
298+
config: {
299+
topic: 'project.digest',
300+
recipients: ['admin@objectos.ai'],
301+
channels: ['inbox'],
302+
severity: 'info',
303+
title: 'Scheduled project digest',
304+
message: 'Your periodic project digest is ready — open Projects for the latest health.',
305+
actionUrl: '/showcase_project',
306+
},
307+
},
308+
{ id: 'end', type: 'end', label: 'End' },
309+
],
310+
edges: [
311+
{ id: 'e1', source: 'start', target: 'digest' },
312+
{ id: 'e2', source: 'digest', target: 'end' },
313+
],
314+
});
315+
316+
/**
317+
* Task Completed → REST Ping (self) — the worked `connector_action` example on
318+
* the generic `rest` connector.
319+
*
320+
* Where {@link TaskCompletedSlackFlow} targets the `slack` connector (which
321+
* needs a real bot token + channel), this flow dispatches to the `rest`
322+
* connector contributed by `@objectstack/connector-rest`, configured to point
323+
* at the running server itself. On task completion it issues
324+
* `GET /api/v1/health`; the request and its `{ status: 'ok' }` response are
325+
* captured on the flow run, so the connector dispatch is fully observable
326+
* without any external service or credentials.
327+
*/
328+
export const TaskCompletedRestPingFlow = defineFlow({
329+
name: 'showcase_task_completed_rest_ping',
330+
label: 'REST Ping on Task Completed',
331+
description: 'Calls the local server health endpoint via the rest connector when a task is marked Done.',
332+
type: 'autolaunched',
333+
nodes: [
334+
{
335+
id: 'start',
336+
type: 'start',
337+
label: 'On Task Update',
338+
config: {
339+
objectName: 'showcase_task',
340+
triggerType: 'record-after-update',
341+
condition: 'status == "done" && previous.status != "done"',
342+
},
343+
},
344+
{
345+
id: 'ping',
346+
type: 'connector_action',
347+
label: 'GET /api/v1/health',
348+
connectorConfig: {
349+
connectorId: 'rest',
350+
actionId: 'request',
351+
input: {
352+
method: 'GET',
353+
path: '/api/v1/health',
354+
},
355+
},
356+
},
357+
{ id: 'end', type: 'end', label: 'End' },
358+
],
359+
edges: [
360+
{ id: 'e1', source: 'start', target: 'ping' },
361+
{ id: 'e2', source: 'ping', target: 'end' },
362+
],
363+
});
364+
261365
export const allFlows = [
262366
TaskCompletedFlow,
263367
ReassignWizardFlow,
264368
BudgetApprovalFlow,
265369
TaskCompletedSlackFlow,
266370
TaskAssignedNotifyFlow,
371+
ScheduledDigestFlow,
372+
TaskCompletedRestPingFlow,
267373
];

pnpm-lock.yaml

Lines changed: 6 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)