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
5 changes: 5 additions & 0 deletions .changeset/hook-designer-retry-timeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@objectstack/spec": patch
---

fix(spec): surface hook `retryPolicy` and `timeout` in the Studio hook designer form (Execution section), completing schema coverage.
5 changes: 4 additions & 1 deletion examples/app-showcase/objectstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ import { allActions } from './src/actions/index.js';
import { ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage } from './src/pages/index.js';
import { allFlows } from './src/flows/index.js';
import { allWebhooks } from './src/webhooks/index.js';
import { allHooks } from './src/hooks/index.js';
import { allJobs } from './src/jobs/index.js';
import { allEmails } from './src/emails/index.js';
import { ShowcaseAssistantAgent, ProjectOpsSkill } from './src/agents/index.js';
import { ShowcaseAssistantAgent, ProjectOpsSkill, allTools } from './src/agents/index.js';
import { allBooks } from './src/books/index.js';
import {
allRoles,
Expand Down Expand Up @@ -154,6 +155,7 @@ export default defineStack({
flows: allFlows,
jobs: allJobs,
emailTemplates: allEmails,
hooks: allHooks,
webhooks: allWebhooks,

// Security
Expand All @@ -165,6 +167,7 @@ export default defineStack({
// AI
agents: [ShowcaseAssistantAgent],
skills: [ProjectOpsSkill],
tools: allTools,

// Seed data
data: ShowcaseSeedData,
Expand Down
23 changes: 22 additions & 1 deletion examples/app-showcase/src/agents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,31 @@ export const FindProjectTool = defineTool({
builtIn: false,
});

/** Tool — object-bound summary that needs confirmation before it runs. */
export const SummarizeProjectTasksTool = defineTool({
name: 'showcase_summarize_project_tasks',
label: 'Summarize Project Tasks',
description: 'Summarise the open tasks for a project, grouped by status.',
objectName: 'showcase_task',
parameters: {
type: 'object',
properties: {
project_id: { type: 'string', description: 'Project record id' },
include_done: { type: 'boolean', description: 'Include completed tasks', default: false },
},
required: ['project_id'],
},
requiresConfirmation: true,
active: true,
builtIn: false,
});

/** Skill — bundles the project tools. */
export const ProjectOpsSkill = defineSkill({
name: 'showcase_project_ops',
label: 'Project Operations',
description: 'Tools and prompts for working with projects and tasks.',
tools: ['showcase_find_project'],
tools: ['showcase_find_project', 'showcase_summarize_project_tasks'],
active: true,
});

Expand All @@ -36,3 +55,5 @@ export const ShowcaseAssistantAgent = defineAgent({
active: true,
visibility: 'global',
});

export const allTools = [FindProjectTool, SummarizeProjectTasksTool];
81 changes: 81 additions & 0 deletions examples/app-showcase/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Object lifecycle hooks — the showcase's "logic layer".
*
* Each hook is a plain object validated by `HookSchema` inside
* `defineStack({ hooks })` (same authoring style as webhooks). Together they
* exercise the full hook designer surface so Studio has something real to
* render for every property:
*
* • multi-event targeting (`beforeInsert` + `beforeUpdate`)
* • an L2 sandboxed-JS `body` (language + source + capabilities)
* • a CEL `condition` gate
* • fire-and-forget `async` execution with a `retryPolicy`
* • `onError` / `priority` tuning across more than one object
*
* The bodies are deliberately tiny and side-effect-light — they are read as
* documentation as much as they run.
*/

type LifecycleEvent =
| 'beforeInsert' | 'afterInsert'
| 'beforeUpdate' | 'afterUpdate'
| 'beforeDelete' | 'afterDelete';

/** beforeInsert/beforeUpdate — normalise the task title before it is stored. */
export const NormalizeTaskTitleHook = {
name: 'showcase_normalize_task_title',
label: 'Normalize Task Title',
object: 'showcase_task',
events: ['beforeInsert', 'beforeUpdate'] as LifecycleEvent[],
body: {
language: 'js' as const,
source: "if (ctx.input.title) ctx.input.title = ctx.input.title.trim();",
},
priority: 50,
onError: 'abort' as const,
description: 'Trims leading/trailing whitespace from the task title before every write.',
};

/** afterUpdate (gated) — log a line whenever a task flips to done. */
export const AuditTaskCompletionHook = {
name: 'showcase_audit_task_completion',
label: 'Audit Task Completion',
object: 'showcase_task',
events: ['afterUpdate'] as LifecycleEvent[],
condition: "record.done == true",
body: {
language: 'js' as const,
source: "var r = ctx.result || ctx.input || {}; if (typeof ctx.log === 'function') ctx.log('task completed: ' + (r.title || r.id || 'unknown'));",
capabilities: ['log'] as ('log')[],
},
async: true,
priority: 90,
retryPolicy: { maxRetries: 3, backoffMs: 1000 },
onError: 'log' as const,
description: 'Fire-and-forget audit line emitted after a task transitions to done.',
};

/** afterUpdate (gated) — warn when a project goes over budget. */
export const WarnOverBudgetHook = {
name: 'showcase_warn_over_budget',
label: 'Warn On Over-Budget Project',
object: 'showcase_project',
events: ['afterUpdate'] as LifecycleEvent[],
condition: "record.spent > record.budget",
body: {
language: 'js' as const,
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 + ')');",
capabilities: ['log'] as ('log')[],
},
async: true,
onError: 'log' as const,
description: 'Emits a warning when a project’s spend exceeds its budget.',
};

export const allHooks = [
NormalizeTaskTitleHook,
AuditTaskCompletionHook,
WarnOverBudgetHook,
];
11 changes: 11 additions & 0 deletions packages/spec/src/data/hook.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,18 @@ export const hookForm = defineForm({
{ label: 'Abort', value: 'abort' },
{ label: 'Log', value: 'log' },
] },
{ field: 'timeout', type: 'number', colSpan: 1, helpText: 'Abort the hook after N milliseconds' },
{ field: 'condition', type: 'code', language: 'javascript', colSpan: 2, helpText: 'Optional formula — skip the hook when this evaluates to false' },
{
field: 'retryPolicy',
type: 'composite',
colSpan: 2,
helpText: 'Retry on failure — most useful for async hooks',
fields: [
{ field: 'maxRetries', type: 'number', helpText: 'Maximum retry attempts' },
{ field: 'backoffMs', type: 'number', helpText: 'Delay between retries (ms)' },
],
},
],
},
],
Expand Down