Skip to Content
TutorialsDeveloper Series8. Event Hooks & Extensions

Tutorial 8: Event Hooks & Extensions

So far, all your logic lives in the frontend. But some rules need to run on the server — validation that can’t be bypassed by a crafty API call, notifications triggered by data changes, and scheduled jobs that run whether or not anyone has the app open.

DaaS provides three extension mechanisms:

  • Filter hooks — blocking middleware that can validate or transform data before an operation completes
  • Action hooks — non-blocking side effects that fire after an operation succeeds
  • Cron jobs — scheduled JavaScript that runs on a timer

By the end you’ll have:

  • A filter hook that rejects tasks with a due_date in the past on creation
  • An action hook that fires a notification when assigned_to changes on a task
  • A cron job that escalates overdue tasks to high priority every day at 8am
  • All hooks visible and debuggable via the DaaS Logs panel (/logs).

Prerequisites

  • Completed Tutorial 7: File Uploads.
  • Familiarity with basic JavaScript (hooks and cron jobs are written as .mjs files).
  • Your AI IDE must have active MCP connections — see AI IDE Setup if not configured.

Runtime vs. file extensions — Runtime extensions are stored in the daas_extensions database table, hot-reload automatically, and are fully manageable by your AI assistant via MCP tools. File-based extensions in extensions/<name>/index.mjs require a server restart and cannot be managed by AI. This tutorial uses runtime extensions.

Create a Filter Hook: Validate Due Date

A filter hook on items.create can inspect the incoming payload and throw an error to abort the operation. DaaS will return that error to the caller — no item is created.

Agent mode required — In VS Code, open Copilot Chat and switch to agent mode (⚡). In Kiro, open the Kiro Chat panel. Your AI has access to the mcp_daas_extensions tool via MCP.

In your AI chat panel, give your AI this prompt:

Create a runtime filter hook named "validate-task-due-date" on the "items.create" event. It should check the payload's due_date field and throw an error if the date is in the past (compare by day only). The hook should only apply to the "tasks" collection. Make it active.

Your AI will create code similar to this:

if (meta.collection !== "tasks") return payload; if (payload.due_date) { const due = new Date(payload.due_date); const now = new Date(); now.setHours(0, 0, 0, 0); if (due < now) { throw new Error("Due date cannot be in the past."); } } return payload;

The hook runs immediately — no restart required.

Structured errors — Use throw new HookError(message, statusCode, code) instead of throw new Error(...) to set a custom HTTP status code and error code on the response. HookError is available in all runtime extensions and cron jobs.

Test it: Try creating a task via the form or API with a due_date yesterday. The request should be rejected with "Due date cannot be in the past.".

Filter hooks run synchronously in the request path. Keep them fast — avoid external HTTP calls inside filter hooks. Use action hooks for side effects.

Create an Action Hook: Assignment Notification

An action hook on items.update fires after the update succeeds. Use it to send a notification when assigned_to changes.

Agent mode required — In VS Code, open Copilot Chat and switch to agent mode (⚡). In Kiro, open the Kiro Chat panel. Your AI has access to the mcp_daas_extensions tool via MCP.

In your AI chat panel, give your AI this prompt:

Create a runtime action hook named "notify-task-assignment" on the "items.update" event. It should only fire for the "tasks" collection and only when "assigned_to" is in the changed payload. For each updated task, read the title and assigned_to fields and log a notification message to the console. Make it active.

Your AI will create code similar to this:

if (meta.collection !== "tasks") return; if (!meta.payload.assigned_to) return; const tasksService = await services.items("tasks"); for (const id of meta.keys) { const task = await tasksService.readOne(id, { fields: ["title", "assigned_to"], }); // Log the assignment — replace with your notification service console.log( `[notify] Task "${task.title}" assigned to user ${task.assigned_to}` ); // Example: POST to an external notification endpoint via services.fetch // await services.fetch("https://your-notifications.example.com/send", { // method: "POST", // headers: { "Content-Type": "application/json" }, // body: JSON.stringify({ // userId: task.assigned_to, // message: `You have been assigned to "${task.title}"`, // }), // }); }

Test it: Edit a task and change assigned_to. Open the DaaS Logs panel at /logs, filter by source extension — you should see the [notify] log message.

HTTP requests from extensions — Use await services.fetch(url, options) instead of the global fetch(). The sandbox blocks fetch for security; services.fetch enforces domain allowlisting configured via the EXTENSION_ALLOWED_DOMAINS environment variable.

Verify Hooks in the Logs Panel

DaaS logs all hook executions. To monitor them live:

  1. In the DaaS admin UI, go to Logs (/logs).
  2. Set source to extension.
  3. Trigger a task creation or update in your app.
  4. Watch logs appear in real time.

DaaS logs panel showing extension hook events in real time

Create a Cron Job: Escalate Overdue Tasks

A cron job runs on a schedule. Create one that sets priority to high for any task that is past its due_date and not yet done.

Agent mode required — In VS Code, open Copilot Chat and switch to agent mode (⚡). In Kiro, open the Kiro Chat panel. Your AI has access to the mcp_daas_cron tool via MCP.

In your AI chat panel, type the slash command:

/create-cron "escalate-overdue-tasks" daily

When prompted, provide the details: timezone Asia/Singapore, runs at 8am (0 8 * * *), escalates overdue tasks by setting priority to high.

Your AI will create code similar to this:

const tasksService = await services.items("tasks"); const today = new Date().toISOString().split("T")[0]; const overdue = await tasksService.readByQuery({ filter: { _and: [ { due_date: { _lt: today } }, { status: { _neq: "done" } }, { priority: { _neq: "high" } }, ], }, fields: ["id", "title"], }); if (overdue.length === 0) { console.log("[escalate] No overdue tasks found."); return; } for (const task of overdue) { await tasksService.updateOne(task.id, { priority: "high" }); console.log(`[escalate] Escalated task "${task.title}" to high priority.`); } console.log(`[escalate] Done. Escalated ${overdue.length} task(s).`);

The cron job will run at its next scheduled time. To test immediately, ask your AI:

Run the escalate-overdue-tasks cron job now and show me the execution logs.

Cron jobs have access to all DaaS services — they can query and mutate any collection. They run with admin-level access and bypass RBAC policies.

Run the Full E2E Check

Verify all hooks work together:

  1. Create a task with due_date = yesterday → should fail with validation error
  2. Create a task with due_date = tomorrow → should succeed

Extension Quick Reference

TypeRunsBlockingUse forScope
Filter hookBefore operationYes — can abortValidation, payload transformationRuntime & file-based
Action hookAfter operationNoNotifications, audit logs, side effectsRuntime & file-based
Cron jobOn a timerNoBatch processing, scheduled checksDatabase-stored
Custom endpointOn HTTP requestN/ACustom REST routesFile-based only

Hook Events Reference

EventTriggered when
items.queryAny read query on a collection
items.readA specific item is read
items.createAn item is created
items.updateAn item is updated
items.deleteAn item is deleted

Congratulations

You’ve built a complete project management app with Buildpad — from data schema to UI, access control, workflows, file uploads, and server-side automation. Here’s what you covered across all 8 tutorials:

TutorialFeature
1Project scaffold + Phase 0 foundation
2Collections & field types
3CollectionList + CollectionForm UI
4RBAC — Manager and Member roles
5Workflow state machine + WorkflowButton
6M2O and M2M relations
7File and image uploads
8Filter hooks, action hooks, cron jobs

Where to Go Next

Last updated on