This guide explains how domain plugins expose their hooks as integration events for external webhook subscriptions.
Integration events bridge platform hooks to external systems. When a domain plugin emits a hook (e.g., incident.created), the integration system can deliver that event to configured webhooks, Slack channels, or other external services.
┌─────────────────────────────────────────────────────────────────┐
│ Domain Plugin │
│ │
│ 1. Define Hook 2. Emit Hook 3. Register as Event │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ createHook() │ -> │ emitHook() │ -> │ registerEvent│ │
│ │ │ │ │ │ (at startup) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Integration Backend │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Event │ -> │ Hook │ -> │ Delivery │ │
│ │ Registry │ │ Subscriber │ │ Coordinator │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ External Systems │
│ (Webhooks, Slack, Jira, PagerDuty, etc.) │
└─────────────────────────────────────────────────────────────────┘
Hooks are defined in your plugin’s hooks.ts:
// src/hooks.ts
import { createHook } from "@checkstack/backend-api";
export const incidentHooks = {
incidentCreated: createHook<{
incidentId: string;
systemIds: string[];
title: string;
severity: string;
}>("incident.created"),
incidentResolved: createHook<{
incidentId: string;
systemIds: string[];
}>("incident.resolved"),
} as const;
In your router or service, emit the hook when the relevant action happens:
// src/router.ts
import { incidentHooks } from "./hooks";
// Inside an RPC handler
const router = os.router({
createIncident: publicProcedure
.input(...)
.mutation(async ({ ctx, input }) => {
// Create the incident
const incident = await db.insert(incidents).values(input).returning();
// Emit the hook - this triggers integration events!
await ctx.emitHook(incidentHooks.incidentCreated, {
incidentId: incident.id,
systemIds: incident.systemIds,
title: incident.title,
severity: incident.severity,
});
return incident;
}),
});
In your plugin’s index.ts, register the hook with the integration extension point:
// src/index.ts
import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
import { integrationEventExtensionPoint } from "@checkstack/integration-backend";
import { z } from "zod";
import { incidentHooks } from "./hooks";
import { pluginMetadata } from "./plugin-metadata";
// Define Zod schemas for payload validation and JSON Schema generation
const incidentCreatedPayload = z.object({
incidentId: z.string(),
systemIds: z.array(z.string()),
title: z.string(),
severity: z.string(),
});
const incidentResolvedPayload = z.object({
incidentId: z.string(),
systemIds: z.array(z.string()),
});
export default createBackendPlugin({
metadata: pluginMetadata,
register(env) {
// Get the integration extension point
const integrationEvents = env.getExtensionPoint(
integrationEventExtensionPoint
);
// Register hooks as integration events
integrationEvents.registerEvent(
{
hook: incidentHooks.incidentCreated,
displayName: "Incident Created",
description: "Fires when a new incident is created",
category: "Incidents",
payloadSchema: incidentCreatedPayload,
},
pluginMetadata
);
integrationEvents.registerEvent(
{
hook: incidentHooks.incidentResolved,
displayName: "Incident Resolved",
description: "Fires when an incident is marked as resolved",
category: "Incidents",
payloadSchema: incidentResolvedPayload,
},
pluginMetadata
);
// ... rest of plugin init
},
});
interface IntegrationEventDefinition<T> {
/** The hook to expose (must be created with createHook()) */
hook: HookReference<T>;
/** Human-readable name shown in UI */
displayName: string;
/** Description of when this event fires */
description?: string;
/** Category for UI grouping */
category?: string; // "Incidents", "Maintenance", "Health Checks", etc.
/** Zod schema for payload (used for validation and JSON Schema) */
payloadSchema: z.ZodType<T>;
/** Optional: Transform payload before sending to webhooks */
transformPayload?: (payload: T) => Record<string, unknown>;
}
Sometimes you want to modify the payload before it’s sent to external systems. Use transformPayload to:
integrationEvents.registerEvent(
{
hook: incidentHooks.incidentCreated,
displayName: "Incident Created",
category: "Incidents",
payloadSchema: incidentCreatedPayload,
// Transform internal format to external-friendly format
transformPayload: (payload) => ({
id: payload.incidentId,
type: "INCIDENT_CREATED",
severity: payload.severity.toUpperCase(),
affected_systems: payload.systemIds.length,
system_ids: payload.systemIds,
title: payload.title,
timestamp: new Date().toISOString(),
}),
},
pluginMetadata
);
Events are automatically namespaced by plugin ID:
| Plugin ID | Hook ID | Full Event ID |
|---|---|---|
incident |
incident.created |
incident.incident.created |
maintenance |
started |
maintenance.started |
healthcheck-http |
state.changed |
healthcheck-http.state.changed |
The integration backend subscribes to registered hooks using work-queue mode:
// Inside integration-backend (automatic)
onHook(
registeredEvent.hook,
async (payload) => {
// Route to all matching subscriptions
await deliveryCoordinator.handleEvent(registeredEvent, payload);
},
{ mode: "work-queue", workerGroup: `integration.${eventId}` }
);
Work-queue mode ensures:
Add the integration packages to your plugin’s package.json:
{
"dependencies": {
"@checkstack/integration-backend": "workspace:*",
"@checkstack/integration-common": "workspace:*"
}
}
Categories group events in the UI. Use consistent naming:
| Category | Description |
|---|---|
Incidents |
Unplanned outages and issues |
Maintenance |
Scheduled maintenance windows |
Health |
Health check state changes |
Catalog |
System and group changes |
Auth |
User and authentication events |
// incident-backend/src/index.ts
import * as schema from "./schema";
import { z } from "zod";
import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
import { integrationEventExtensionPoint } from "@checkstack/integration-backend";
import { pluginMetadata, incidentContract } from "@checkstack/incident-common";
import { incidentHooks } from "./hooks";
import { createRouter } from "./router";
// Payload schemas for integration events
const incidentCreatedPayload = z.object({
incidentId: z.string(),
systemIds: z.array(z.string()),
title: z.string(),
severity: z.string(),
});
const incidentUpdatedPayload = z.object({
incidentId: z.string(),
systemIds: z.array(z.string()),
statusChange: z.string().optional(),
});
const incidentResolvedPayload = z.object({
incidentId: z.string(),
systemIds: z.array(z.string()),
});
export default createBackendPlugin({
metadata: pluginMetadata,
register(env) {
// Register integration events in the register phase
const integrationEvents = env.getExtensionPoint(
integrationEventExtensionPoint
);
integrationEvents.registerEvent({
hook: incidentHooks.incidentCreated,
displayName: "Incident Created",
description: "Fires when a new incident is created",
category: "Incidents",
payloadSchema: incidentCreatedPayload,
}, pluginMetadata);
integrationEvents.registerEvent({
hook: incidentHooks.incidentUpdated,
displayName: "Incident Updated",
description: "Fires when an incident is updated",
category: "Incidents",
payloadSchema: incidentUpdatedPayload,
}, pluginMetadata);
integrationEvents.registerEvent({
hook: incidentHooks.incidentResolved,
displayName: "Incident Resolved",
description: "Fires when an incident is resolved",
category: "Incidents",
payloadSchema: incidentResolvedPayload,
}, pluginMetadata);
// Continue with normal plugin initialization
env.registerInit({
schema,
deps: { logger: coreServices.logger, rpc: coreServices.rpc },
init: async ({ logger, database, rpc }) => {
// ... router setup
},
});
},
});
integrationEventExtensionPoint import pathemitHook() calls in your codeemitHook() payload matches schema