Skip to Content
AutomateExtensions

Extensions

Extensions add custom event hooks and REST endpoints to the platform. There are two types:

TypeLocationReloadAccess
RuntimeStored in daas_extensions tableHot-reload, no deploymentAll users
File-basedextensions/<name>/index.mjsRequires server restartRequires Docker image access

Event Names

Both extension types subscribe to the same events. Use a specific collection prefix for targeted hooks, or the global form to catch all collections.

PatternExampleDescription
items.createCreate in any collection
items.updateUpdate in any collection
items.deleteDelete in any collection
items.readRead in any collection
items.queryQuery (filter-only)
{collection}.items.createarticles.items.createCreate in specific collection
{collection}.items.updatearticles.items.updateUpdate in specific collection
versions.createContent version created
xtr.{category}.{action}xtr.item.promoteWorkflow action event

Runtime Extensions

Runtime extensions are stored in the daas_extensions database table and hot-reloaded without deployment. They are ideal for dynamic business logic managed via MCP or the API.

Database schema (daas_extensions)

ColumnTypeDescription
iduuidPrimary key
nametextHuman-readable name
descriptiontextWhat the extension does
eventtextEvent name to subscribe to
typetextfilter or action
codetextJavaScript code body
statustextactive, inactive, or error
sortintegerExecution order
timeout_msintegerExecution timeout in ms (default 5000)
memory_limit_mbintegerMemory limit in MB (default 64)
last_errortextLast error message, if any
last_error_attimestamptzWhen the last error occurred
execution_countintegerTotal number of executions
last_executed_attimestamptzLast execution timestamp
avg_execution_time_msnumericAverage execution time

Code context

Filter hooks receive (payload, meta, context) and must return the payload:

// Auto-generate slug from title if (!payload.slug && payload.title) { payload.slug = payload.title.toLowerCase().replace(/\s+/g, '-'); } return payload;

Action hooks receive (meta, context) and the return value is ignored:

// Log creation console.log(`Item created in ${meta.collection} by ${context.accountability?.user}`);

Both have access to context.services:

// Items — CRUD on any collection const svc = await context.services.items('articles'); const items = await svc.readByQuery({ filter: { status: { _eq: 'published' } } }); await svc.createOne({ title: 'New Article' }); await svc.updateOne(id, { status: 'draft' }); // Other services const cols = await context.services.collections(); const flds = await context.services.fields(); const files = await context.services.files(); const vers = await context.services.versions(); const rels = await context.services.relations(); // Send email await context.services.mail({ to: 'user@example.com', subject: 'Hi', html: '<p>Hello</p>' }); // Trigger / list cron jobs await context.services.cron.trigger('job-id-or-name'); const jobs = await context.services.cron.list(); // Whitelisted environment variables const apiKey = context.services.env.MY_API_KEY; // Safe HTTP (domain-restricted — configure EXTENSION_ALLOWED_DOMAINS) const res = await context.services.fetch('https://api.example.com/data'); // Raw Supabase client (bypasses RLS — use with caution) const { data } = await context.services.supabase.from('table').select('*');

Managing via MCP

The extensions MCP tool lets AI agents manage runtime extensions:

// Create { "action": "create", "name": "Auto-slug", "event": "articles.items.create", "type": "filter", "code": "...", "status": "inactive" } // Test before activating { "action": "test", "code": "return { ...payload, tested: true };", "type": "filter", "event": "articles.items.create", "testPayload": { "title": "Hello World" } } // Activate { "action": "activate", "id": "extension-uuid" } // List { "action": "list", "status": "active" }

Always test an extension before activating it. A broken filter hook can block write operations for the affected collection.

File-Based Extensions

File-based extensions require access to the BuildPad Docker image to mount the extensions/ directory. Contact the BuildPad team to get access.

File-based extensions are JavaScript modules placed in the extensions/ directory. They are loaded at server startup and require a restart to pick up changes.

Structure

extensions/ └── my-extension/ └── index.mjs

Module format

Every extension exports a register(sdk) function:

// extensions/my-extension/index.mjs export function register(sdk) { const { emitter, createItemsService, registerRoutes } = sdk; // Action hook — runs after item creation emitter.onAction('articles.items.create', async (meta, context) => { console.log('Article created:', meta.key); }); // Filter hook — runs before item creation, can modify payload emitter.onFilter('articles.items.create', async (payload, meta, context) => { if (!payload.title) throw new Error('Title is required'); return { ...payload, slug: payload.title.toLowerCase().replace(/\s+/g, '-'), }; }); }

Extensions are loaded at startup. Restart the server to pick up new file-based extensions.

SDK reference

SDK exportDescription
emitter.onAction(event, handler)Register non-blocking action hook
emitter.onFilter(event, handler)Register blocking filter hook
emitter.offAction(event, handler)Unregister action hook
emitter.offFilter(event, handler)Unregister filter hook
createItemsService(collection)Create an ItemsService instance
createVersionService()Create a VersionService instance
registerRoutes(config)Register custom REST endpoints

Custom REST endpoints

export function register(sdk) { sdk.registerRoutes({ name: 'my-extension', basePath: '/customers', // accessible at /api/ext/customers/* description: 'Customer API', routes: [ { method: 'GET', path: '/', description: 'List customers', requiredPermission: 'customers.read', handler: async (request, context) => { const { supabase, accountability } = context; const { data } = await supabase.from('customers').select('*'); return Response.json({ data }); }, }, { method: 'POST', path: '/', requiredPermission: 'customers.create', handler: async (request, context) => { const body = await request.json(); // ... create logic return Response.json({ data: customer }, { status: 201 }); }, }, ], }); }

All extension routes are available at /api/ext/{basePath}/.

TypeScript types

import type { FilterHandler, ActionHandler, EventContext, Accountability, } from '@microbuild/sdk';
Last updated on