Skip to content

Add objectstack-hooks skill guide for third-party developers#1119

Merged
hotlong merged 1 commit intomainfrom
claude/create-skill-guide-hooks
Apr 13, 2026
Merged

Add objectstack-hooks skill guide for third-party developers#1119
hotlong merged 1 commit intomainfrom
claude/create-skill-guide-hooks

Conversation

@Claude
Copy link
Copy Markdown
Contributor

@Claude Claude AI commented Apr 13, 2026

Created comprehensive documentation for third-party developers writing data lifecycle hooks in ObjectStack.

Files Added

  • skills/objectstack-hooks/SKILL.md (1,038 lines) — Complete guide covering:

    • 14 lifecycle events (beforeInsert, afterUpdate, etc.)
    • Hook definition schema (properties: name, object, events, handler, priority, async, condition, etc.)
    • HookContext API reference (input, result, previous, session, api, etc.)
    • 10+ production-ready patterns (validation, enrichment, side effects, audit trails, external APIs)
    • Registration methods (declarative, programmatic, file-based)
    • Best practices, error handling, testing, performance optimization
    • Troubleshooting guide
  • skills/objectstack-hooks/references/ — Schema references and documentation index

Example Usage

import { Hook, HookContext } from '@objectstack/spec/data';

const accountHook: Hook = {
  name: 'account_validation',
  object: 'account',
  events: ['beforeInsert', 'beforeUpdate'],
  handler: async (ctx: HookContext) => {
    // Set defaults
    if (!ctx.input.industry) {
      ctx.input.industry = 'Other';
    }
    
    // Validate
    if (ctx.input.website && !ctx.input.website.startsWith('http')) {
      throw new Error('Website must start with http or https');
    }
    
    // Enrich with cross-object data
    const settings = await ctx.api?.object('settings').findOne({
      filter: { tenant_id: ctx.session?.tenantId }
    });
  },
  priority: 100,
  async: false,
};

Structure

Follows existing skill conventions (objectstack-data, objectstack-kernel, etc.) with:

  • Metadata frontmatter (name, description, tags)
  • "When to Use This Skill" trigger conditions
  • Core concepts and API reference
  • Practical examples from real codebase (app-todo, app-crm)
  • References to canonical schemas in @objectstack/spec

Complements existing skills: objectstack-data (objects/fields), objectstack-kernel (plugins/services), objectstack-automation (flows/workflows).

Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/65fb3af8-127a-49b6-a200-29a63b6a8073

Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Apr 13, 2026 11:18am
spec Ready Ready Preview, Comment Apr 13, 2026 11:18am

Request Review

@github-actions github-actions bot added the documentation Improvements or additions to documentation label Apr 13, 2026
@hotlong hotlong marked this pull request as ready for review April 13, 2026 11:18
Copilot AI review requested due to automatic review settings April 13, 2026 11:18
@hotlong hotlong merged commit 244e624 into main Apr 13, 2026
12 of 14 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new objectstack-hooks skill to document how third-party developers can implement ObjectStack data lifecycle hooks, including schema references intended to mirror @objectstack/spec.

Changes:

  • Added a comprehensive hooks skill guide (skills/objectstack-hooks/SKILL.md) with lifecycle event docs, patterns, and example code.
  • Added a schema reference snapshot for hooks (skills/objectstack-hooks/references/data/hook.zod.ts).
  • Added a references index (skills/objectstack-hooks/references/_index.md) for navigating the snapshots.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 10 comments.

File Description
skills/objectstack-hooks/SKILL.md New end-to-end guide for authoring and registering hooks, plus patterns and testing guidance.
skills/objectstack-hooks/references/data/hook.zod.ts Local “snapshot” of the hook Zod schemas for quick reference.
skills/objectstack-hooks/references/_index.md Index page describing the available reference schema snapshots.

Comment on lines +51 to +65
ObjectStack provides **14 lifecycle events** organized by operation type:

| Event | When It Fires | Use Cases |
|:------|:--------------|:----------|
| **Read Operations** | | |
| `beforeFind` | Before querying multiple records | Filter queries by user context, log access |
| `afterFind` | After querying multiple records | Transform results, mask sensitive data |
| `beforeFindOne` | Before fetching a single record | Validate permissions, log access |
| `afterFindOne` | After fetching a single record | Enrich data, mask fields |
| `beforeCount` | Before counting records | Filter by context |
| `afterCount` | After counting records | Log metrics |
| `beforeAggregate` | Before aggregate operations | Validate aggregation rules |
| `afterAggregate` | After aggregate operations | Transform results |
| **Write Operations** | | |
| `beforeInsert` | Before creating a record | Set defaults, validate, normalize |
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guide states ObjectStack provides “14 lifecycle events”, but the canonical HookEvent enum includes additional bulk events (before/afterUpdateMany, before/afterDeleteMany) in @objectstack/spec, and the current ObjectQL engine only triggers a smaller subset (before/afterFind/Insert/Update/Delete). Please update this section to either (a) list all events from HookEvent and clearly mark which are currently emitted by the engine, or (b) scope the guide explicitly to the events that are actually triggered today.

Suggested change
ObjectStack provides **14 lifecycle events** organized by operation type:
| Event | When It Fires | Use Cases |
|:------|:--------------|:----------|
| **Read Operations** | | |
| `beforeFind` | Before querying multiple records | Filter queries by user context, log access |
| `afterFind` | After querying multiple records | Transform results, mask sensitive data |
| `beforeFindOne` | Before fetching a single record | Validate permissions, log access |
| `afterFindOne` | After fetching a single record | Enrich data, mask fields |
| `beforeCount` | Before counting records | Filter by context |
| `afterCount` | After counting records | Log metrics |
| `beforeAggregate` | Before aggregate operations | Validate aggregation rules |
| `afterAggregate` | After aggregate operations | Transform results |
| **Write Operations** | | |
| `beforeInsert` | Before creating a record | Set defaults, validate, normalize |
The canonical `HookEvent` enum in `@objectstack/spec` defines additional lifecycle
events, including bulk operations such as `beforeUpdateMany`, `afterUpdateMany`,
`beforeDeleteMany`, and `afterDeleteMany`.
**This guide is scoped to the lifecycle events currently emitted by the ObjectQL engine
today**, which are listed below:
| Event | When It Fires | Use Cases |
|:------|:--------------|:----------|
| **Read Operations** | | |
| `beforeFind` | Before querying multiple records | Filter queries by user context, log access |
| `afterFind` | After querying multiple records | Transform results, mask sensitive data |
| **Write Operations** | | |
| `beforeInsert` | Before creating a record | Set defaults, validate, normalize |
| `afterInsert` | After creating a record | Trigger side effects, log creation |
| `beforeUpdate` | Before updating a record | Validate changes, normalize values |
| `afterUpdate` | After updating a record | Publish events, sync external systems |
| `beforeDelete` | Before deleting a record | Enforce business rules, prevent unsafe deletes |
| `afterDelete` | After deleting a record | Clean up related data, emit audit events |

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +81
| **Purpose** | Validation, enrichment, transformation | Side effects, notifications, logging |
| **Can modify** | `ctx.input` (mutable) | `ctx.result` (mutable) |
| **Can abort** | Yes (throw error → rollback) | No (operation already committed) |
| **Transaction** | Within transaction | After transaction (unless async: false) |
| **Error handling** | Aborts operation by default | Logged by default (configurable) |

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Before vs After Hooks” table claims after* hooks “cannot abort” and that errors are “logged by default”. In the current ObjectQL implementation, hook handlers are awaited and exceptions propagate (triggerHooks does not catch), so an after* hook can still fail the operation (and may cause a rollback if running inside a transaction). Please adjust this guidance to reflect actual runtime behavior or document the exact error-handling semantics/guarantees.

Suggested change
| **Purpose** | Validation, enrichment, transformation | Side effects, notifications, logging |
| **Can modify** | `ctx.input` (mutable) | `ctx.result` (mutable) |
| **Can abort** | Yes (throw error → rollback) | No (operation already committed) |
| **Transaction** | Within transaction | After transaction (unless async: false) |
| **Error handling** | Aborts operation by default | Logged by default (configurable) |
| **Purpose** | Validation, enrichment, transformation | Post-operation enrichment, side effects, notifications, logging |
| **Can modify** | `ctx.input` (mutable) | `ctx.result` (mutable) |
| **Can abort / fail operation** | Yes — throwing aborts the operation | Yes — handlers are awaited, so throwing can still fail the overall operation |
| **Transaction** | Typically runs before persistence within the active transaction | Depends on runtime execution scope; may still participate in rollback if executed inside the active transaction |
| **Error handling** | Thrown errors propagate by default | Thrown errors also propagate by default unless explicitly caught by the runtime/caller |
**Important:** In the current ObjectQL implementation, hook handlers are awaited and exceptions from `after*` hooks are not swallowed by `triggerHooks`. Treat `after*` hooks as part of the operation's observable failure path unless your runtime explicitly catches and handles those errors.

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +105
// Required: Handler function (inline or string reference)
handler: async (ctx: HookContext) => {
// Your logic here
},

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the HookSchema example, handler is marked as required, but in the canonical HookSchema (@objectstack/spec/data), handler is optional. This mismatch can confuse implementers (e.g., hooks that are declared for later binding). Please align the documentation/sample with the actual schema requirements.

Copilot uses AI. Check for mistakes.
Comment on lines +193 to +203
#### `async` — Background Execution

Only applicable for `after*` events:

```typescript
// Blocking (default) — runs within transaction
async: false

// Fire-and-forget — runs in background
async: true
```
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section describes async: true as “fire-and-forget” background execution and implies it won’t block transactions. The current ObjectQL engine awaits all hook handlers and does not implement background scheduling based on the HookSchema async flag. Please either remove/soften these guarantees or document the concrete runtime mechanism that executes async hooks (e.g., job queue integration) and where it is implemented.

Copilot uses AI. Check for mistakes.
Comment on lines +286 to +293
The structure of `ctx.input` varies by event:

**Insert operations:**
```typescript
// beforeInsert, afterInsert
{
// All field values being inserted
name: 'Acme Corp',
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The examples document ctx.input as if it contains record fields directly (e.g., ctx.input.name, ctx.input.status). In the current ObjectQL engine, hook context input is shaped as an envelope (e.g., insert: { data, options }, update: { id, data, options }, find: { ast, options }). Please update the examples (and the “structure varies by event” section) to match the actual runtime shape so hooks don’t silently read/write the wrong fields.

Copilot uses AI. Check for mistakes.
Comment on lines +374 to +383
### Cross-Object API

Access other objects within the same transaction:

```typescript
handler: async (ctx: HookContext) => {
// Get API for another object
const users = ctx.api?.object('user');

// Query users
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guide presents ctx.api as the recommended cross-object CRUD API inside hooks. The current ObjectQL hook context construction does not populate api, so this will be undefined in practice. Please either document how to obtain a ScopedContext/ObjectRepository from ctx.ql (or how api is injected), or clearly note that ctx.api is not currently available by default.

Copilot uses AI. Check for mistakes.
Comment on lines +692 to +699
// In your plugin's onEnable()
export const onEnable = async (ctx: { ql: ObjectQL }) => {
ctx.ql.registerHook('beforeInsert', async (hookCtx) => {
// Handler logic
}, {
object: 'account',
priority: 100,
});
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the programmatic registration example, the hook sets ctx.input.created_at, but the engine’s hook input for insert operations is enveloped (e.g., ctx.input.data). As written, this example won’t set the timestamp on the inserted record. Please adjust to write to the actual insert payload shape used by ObjectQL hooks.

Copilot uses AI. Check for mistakes.
Comment on lines +348 to +351
result: {
records: [{ id: '1', ... }, { id: '2', ... }],
total: 150,
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HookContext result examples show afterFind returning an object like { records, total }, but the current ObjectQL engine passes the raw array result into ctx.result for find operations. Please align these result shape examples with the engine’s behavior (or specify which API layer returns { records, total }).

Suggested change
result: {
records: [{ id: '1', ... }, { id: '2', ... }],
total: 150,
}
result: [
{ id: '1', ... },
{ id: '2', ... },
]

Copilot uses AI. Check for mistakes.
Comment on lines +552 to +555

await ctx.api?.object('audit_log').insert({
object_type: ctx.object,
record_id: String(ctx.input.id || ''),
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the audit trail example, record_id is derived from ctx.input.id, but for inserts the record id is only available on ctx.result after creation. As written, afterInsert audit records may store an empty record_id. Please use the appropriate source for each event (result.id for inserts; input.id for updates/deletes).

Suggested change
await ctx.api?.object('audit_log').insert({
object_type: ctx.object,
record_id: String(ctx.input.id || ''),
const recordId = ctx.event === 'afterInsert'
? ctx.result?.id
: ctx.input.id;
await ctx.api?.object('audit_log').insert({
object_type: ctx.object,
record_id: String(recordId || ''),

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +124
// Optional: Conditional execution
condition: "status = 'active' AND amount > 1000",

// Optional: Human-readable description
description: 'Validates account data before save',

// Optional: Error handling strategy
onError: 'abort', // 'abort' | 'log'

// Optional: Execution timeout (ms)
timeout: 5000,

// Optional: Retry policy
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guide presents onError / timeout / retryPolicy / condition as runtime behavior controls, but the current ObjectQL hook execution path doesn’t apply these (no condition evaluation, timeout enforcement, retry logic, or per-hook error policy in triggerHooks). Please either document them as schema-only fields that need additional runtime support, or link to the implementation that applies them so developers know when they take effect.

Suggested change
// Optional: Conditional execution
condition: "status = 'active' AND amount > 1000",
// Optional: Human-readable description
description: 'Validates account data before save',
// Optional: Error handling strategy
onError: 'abort', // 'abort' | 'log'
// Optional: Execution timeout (ms)
timeout: 5000,
// Optional: Retry policy
// Optional schema field: conditional execution metadata.
// Note: The default ObjectQL hook execution path does not currently evaluate
// `condition` automatically; additional runtime support is required.
condition: "status = 'active' AND amount > 1000",
// Optional: Human-readable description
description: 'Validates account data before save',
// Optional schema field: error handling metadata.
// Note: Per-hook `onError` behavior is not currently enforced automatically
// by the default ObjectQL `triggerHooks` execution path.
onError: 'abort', // 'abort' | 'log'
// Optional schema field: timeout metadata.
// Note: Timeouts are not currently enforced automatically by the default
// ObjectQL hook execution path.
timeout: 5000,
// Optional schema field: retry metadata.
// Note: Retry behavior is not currently applied automatically by the default
// ObjectQL hook execution path.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants