Skip to content

Commit 6b6c065

Browse files
authored
Merge pull request #1863 from objectstack-ai/showcase-examples
feat(showcase): fill missing designer examples — hooks + AI tools (+ hook designer coverage)
2 parents 3424cbf + 1ba9d14 commit 6b6c065

5 files changed

Lines changed: 123 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): surface hook `retryPolicy` and `timeout` in the Studio hook designer form (Execution section), completing schema coverage.

examples/app-showcase/objectstack.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ import { allActions } from './src/actions/index.js';
2121
import { ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage } from './src/pages/index.js';
2222
import { allFlows } from './src/flows/index.js';
2323
import { allWebhooks } from './src/webhooks/index.js';
24+
import { allHooks } from './src/hooks/index.js';
2425
import { allJobs } from './src/jobs/index.js';
2526
import { allEmails } from './src/emails/index.js';
26-
import { ShowcaseAssistantAgent, ProjectOpsSkill } from './src/agents/index.js';
27+
import { ShowcaseAssistantAgent, ProjectOpsSkill, allTools } from './src/agents/index.js';
2728
import { allBooks } from './src/books/index.js';
2829
import {
2930
allRoles,
@@ -154,6 +155,7 @@ export default defineStack({
154155
flows: allFlows,
155156
jobs: allJobs,
156157
emailTemplates: allEmails,
158+
hooks: allHooks,
157159
webhooks: allWebhooks,
158160

159161
// Security
@@ -165,6 +167,7 @@ export default defineStack({
165167
// AI
166168
agents: [ShowcaseAssistantAgent],
167169
skills: [ProjectOpsSkill],
170+
tools: allTools,
168171

169172
// Seed data
170173
data: ShowcaseSeedData,

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,31 @@ export const FindProjectTool = defineTool({
1717
builtIn: false,
1818
});
1919

20+
/** Tool — object-bound summary that needs confirmation before it runs. */
21+
export const SummarizeProjectTasksTool = defineTool({
22+
name: 'showcase_summarize_project_tasks',
23+
label: 'Summarize Project Tasks',
24+
description: 'Summarise the open tasks for a project, grouped by status.',
25+
objectName: 'showcase_task',
26+
parameters: {
27+
type: 'object',
28+
properties: {
29+
project_id: { type: 'string', description: 'Project record id' },
30+
include_done: { type: 'boolean', description: 'Include completed tasks', default: false },
31+
},
32+
required: ['project_id'],
33+
},
34+
requiresConfirmation: true,
35+
active: true,
36+
builtIn: false,
37+
});
38+
2039
/** Skill — bundles the project tools. */
2140
export const ProjectOpsSkill = defineSkill({
2241
name: 'showcase_project_ops',
2342
label: 'Project Operations',
2443
description: 'Tools and prompts for working with projects and tasks.',
25-
tools: ['showcase_find_project'],
44+
tools: ['showcase_find_project', 'showcase_summarize_project_tasks'],
2645
active: true,
2746
});
2847

@@ -36,3 +55,5 @@ export const ShowcaseAssistantAgent = defineAgent({
3655
active: true,
3756
visibility: 'global',
3857
});
58+
59+
export const allTools = [FindProjectTool, SummarizeProjectTasksTool];
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Object lifecycle hooks — the showcase's "logic layer".
5+
*
6+
* Each hook is a plain object validated by `HookSchema` inside
7+
* `defineStack({ hooks })` (same authoring style as webhooks). Together they
8+
* exercise the full hook designer surface so Studio has something real to
9+
* render for every property:
10+
*
11+
* • multi-event targeting (`beforeInsert` + `beforeUpdate`)
12+
* • an L2 sandboxed-JS `body` (language + source + capabilities)
13+
* • a CEL `condition` gate
14+
* • fire-and-forget `async` execution with a `retryPolicy`
15+
* • `onError` / `priority` tuning across more than one object
16+
*
17+
* The bodies are deliberately tiny and side-effect-light — they are read as
18+
* documentation as much as they run.
19+
*/
20+
21+
type LifecycleEvent =
22+
| 'beforeInsert' | 'afterInsert'
23+
| 'beforeUpdate' | 'afterUpdate'
24+
| 'beforeDelete' | 'afterDelete';
25+
26+
/** beforeInsert/beforeUpdate — normalise the task title before it is stored. */
27+
export const NormalizeTaskTitleHook = {
28+
name: 'showcase_normalize_task_title',
29+
label: 'Normalize Task Title',
30+
object: 'showcase_task',
31+
events: ['beforeInsert', 'beforeUpdate'] as LifecycleEvent[],
32+
body: {
33+
language: 'js' as const,
34+
source: "if (ctx.input.title) ctx.input.title = ctx.input.title.trim();",
35+
},
36+
priority: 50,
37+
onError: 'abort' as const,
38+
description: 'Trims leading/trailing whitespace from the task title before every write.',
39+
};
40+
41+
/** afterUpdate (gated) — log a line whenever a task flips to done. */
42+
export const AuditTaskCompletionHook = {
43+
name: 'showcase_audit_task_completion',
44+
label: 'Audit Task Completion',
45+
object: 'showcase_task',
46+
events: ['afterUpdate'] as LifecycleEvent[],
47+
condition: "record.done == true",
48+
body: {
49+
language: 'js' as const,
50+
source: "var r = ctx.result || ctx.input || {}; if (typeof ctx.log === 'function') ctx.log('task completed: ' + (r.title || r.id || 'unknown'));",
51+
capabilities: ['log'] as ('log')[],
52+
},
53+
async: true,
54+
priority: 90,
55+
retryPolicy: { maxRetries: 3, backoffMs: 1000 },
56+
onError: 'log' as const,
57+
description: 'Fire-and-forget audit line emitted after a task transitions to done.',
58+
};
59+
60+
/** afterUpdate (gated) — warn when a project goes over budget. */
61+
export const WarnOverBudgetHook = {
62+
name: 'showcase_warn_over_budget',
63+
label: 'Warn On Over-Budget Project',
64+
object: 'showcase_project',
65+
events: ['afterUpdate'] as LifecycleEvent[],
66+
condition: "record.spent > record.budget",
67+
body: {
68+
language: 'js' as const,
69+
source: "var r = ctx.result || ctx.input || {}; if (typeof ctx.log === 'function') ctx.log('project over budget: ' + (r.name || r.id || 'unknown') + ' (' + r.spent + ' / ' + r.budget + ')');",
70+
capabilities: ['log'] as ('log')[],
71+
},
72+
async: true,
73+
onError: 'log' as const,
74+
description: 'Emits a warning when a project’s spend exceeds its budget.',
75+
};
76+
77+
export const allHooks = [
78+
NormalizeTaskTitleHook,
79+
AuditTaskCompletionHook,
80+
WarnOverBudgetHook,
81+
];

packages/spec/src/data/hook.form.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,18 @@ export const hookForm = defineForm({
6767
{ label: 'Abort', value: 'abort' },
6868
{ label: 'Log', value: 'log' },
6969
] },
70+
{ field: 'timeout', type: 'number', colSpan: 1, helpText: 'Abort the hook after N milliseconds' },
7071
{ field: 'condition', type: 'code', language: 'javascript', colSpan: 2, helpText: 'Optional formula — skip the hook when this evaluates to false' },
72+
{
73+
field: 'retryPolicy',
74+
type: 'composite',
75+
colSpan: 2,
76+
helpText: 'Retry on failure — most useful for async hooks',
77+
fields: [
78+
{ field: 'maxRetries', type: 'number', helpText: 'Maximum retry attempts' },
79+
{ field: 'backoffMs', type: 'number', helpText: 'Delay between retries (ms)' },
80+
],
81+
},
7182
],
7283
},
7384
],

0 commit comments

Comments
 (0)