Event-Driven Handlers
defineEventBridgeHandler creates Lambda handlers triggered by EventBridge events. Import from @j0nathan-ll0yd/core.
Pattern
import { defineEventBridgeHandler } from "@j0nathan-ll0yd/core";
import { exportToS3 } from "@j0nathan-ll0yd/aws";
import { emitEvent } from "@j0nathan-ll0yd/core";
import { getRequiredEnv } from "@j0nathan-ll0yd/env";
const eb = defineEventBridgeHandler({
detailTypes: ["ExportHealthData"],
timeout: 60,
operationName: "ExportHealthData",
});
export const handler = eb(async ({ detail, detailType }) => {
const bucket = getRequiredEnv("DATA_BUCKET");
const generatedAt = new Date().toISOString();
await exportToS3({ bucket, key: `health/${generatedAt}.json`, data: detail });
await emitEvent({ detailType: "ExportHealthDataCompleted", detail: { generatedAt } });
return { generatedAt, detailType };
});Options
| Option | Type | Default | Description |
|---|---|---|---|
detailTypes | string[] | (required) | Detail types this handler responds to |
source | string | -- | Event source for filtering |
operationName | string | function name | Name for metrics and tracing |
timeout | number | -- | Lambda timeout in seconds |
memorySize | number | 128 | Lambda memory in MB |
reservedConcurrency | number | -- | Reserved concurrent executions |
ephemeralStorage | number | 512 | Ephemeral storage in MB |
deadLetterQueue | boolean | { targetArn? } | -- | true = auto-generate SQS DLQ |
retryAttempts | number | -- | Max retry attempts (0-2) |
Handler Params
interface EventBridgeHandlerParams<TDetailType, TDetail> {
event: EventBridgeEvent<TDetailType, TDetail>;
context: Context;
metadata: WrapperMetadata;
detailType: TDetailType; // The detail-type from the event
detail: TDetail; // The event detail payload
source: string; // The event source
}Event Routing
The detailTypes array tells the CLI which EventBridge rules to generate. Each handler responds only to the listed detail types. Multiple handlers can share the same event bus but respond to different detail types.
// Responds to order events
const eb = defineEventBridgeHandler({ detailTypes: ["OrderPlaced", "OrderUpdated"] });
// Responds to export completion events
const eb2 = defineEventBridgeHandler({ detailTypes: ["ExportCompleted"] });Rule naming
One EventBridge rule is generated per function per detail-type, named {stage}-{FunctionName}-{DetailType}. Several handlers may subscribe to the same detail-type; each gets its own rule and its own target.
Migrating to per-function rule names
Rules were previously named {stage}-{DetailType}, with no function segment. When two or more handlers subscribed to one detail-type, each declared its own aws_cloudwatch_event_rule resource and all of them resolved to a single physical rule in AWS. Whichever resource applied last owned the rule; the others silently lost their target, and destroying any one of them destroyed the rule the rest still pointed at.
An app with at most one subscriber per detail-type needs no migration beyond the ordinary apply: the rule is renamed, which OpenTofu performs as a destroy-and-create.
An app with two or more subscribers to any detail-type must migrate its state, because an unmigrated apply destroys the shared rule and creates the new ones, dropping every event published in that window.
List the affected addresses:
bashtofu state list | grep aws_cloudwatch_event_ruleRegenerate and read the plan WITHOUT applying. Confirm the only rule changes are renames:
bashnpx mantle deploy --stage staging --dry-runFor each subscriber, drop the old state entry and import the rule under its new physical name. Drop first, import second — importing over a live entry fails:
bashtofu state rm 'aws_cloudwatch_event_rule.compose_llm_content_broadcast_update' tofu import 'aws_cloudwatch_event_rule.compose_llm_content_broadcast_update' \ 'staging-event-bus/staging-ComposeLlmContent-BroadcastUpdate'The import id is
{bus_name}/{rule_name}for a rule on a custom bus.Re-plan. The rules should show no changes; the targets will show creates for the subscribers that previously lost theirs to the last writer.
Rule names are capped at 64 characters by AWS. A long {stage}-{FunctionName}-{DetailType} combination fails at apply with an explicit error rather than silently truncating.
Publishing Follow-up Events
Use emitEvent from @j0nathan-ll0yd/core to publish events back to the bus:
import { emitEvent } from "@j0nathan-ll0yd/core";
await emitEvent({
detailType: "ProcessingCompleted",
detail: { recordsProcessed: 42, completedAt: new Date().toISOString() },
});emitEvent uses the EVENT_BUS_NAME environment variable injected by the CLI. The bus name is configured in mantle.config.ts:
export default defineConfig({
eventbridge: { bus: "my-app-events" },
});Exporting to S3
Use exportToS3 from @j0nathan-ll0yd/aws to write JSON exports:
import { exportToS3 } from "@j0nathan-ll0yd/aws";
await exportToS3({
bucket: getRequiredEnv("DATA_BUCKET"),
key: `exports/${new Date().toISOString()}.json`,
data: { generatedAt, records },
});Built-in Observability
The handler automatically:
- Adds X-Ray annotations for
detailTypeandeventSource - Logs a structured entry with the detail type and source
- Extracts correlation IDs from the event detail
File Location
EventBridge handlers live under src/lambdas/eventbridge/<DetailType>/index.ts:
src/lambdas/eventbridge/
ExportHealthData/
index.ts
ProcessOrder/
index.tsSee Also
- Handler Patterns — overview of all handler types
- Scheduled Tasks — time-based triggers