Skip to content

Event-Driven Handlers ​

defineEventBridgeHandler creates Lambda handlers triggered by EventBridge events. Import from @j0nathan-ll0yd/core.

Pattern ​

typescript
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 ​

OptionTypeDefaultDescription
detailTypesstring[](required)Detail types this handler responds to
sourcestring--Event source for filtering
operationNamestringfunction nameName for metrics and tracing
timeoutnumber--Lambda timeout in seconds
memorySizenumber128Lambda memory in MB
reservedConcurrencynumber--Reserved concurrent executions
ephemeralStoragenumber512Ephemeral storage in MB
deadLetterQueueboolean | { targetArn? }--true = auto-generate SQS DLQ
retryAttemptsnumber--Max retry attempts (0-2)

Handler Params ​

typescript
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.

typescript
// 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.

  1. List the affected addresses:

    bash
    tofu state list | grep aws_cloudwatch_event_rule
  2. Regenerate and read the plan WITHOUT applying. Confirm the only rule changes are renames:

    bash
    npx mantle deploy --stage staging --dry-run
  3. For 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:

    bash
    tofu 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.

  4. 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:

typescript
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:

typescript
export default defineConfig({
  eventbridge: { bus: "my-app-events" },
});

Exporting to S3 ​

Use exportToS3 from @j0nathan-ll0yd/aws to write JSON exports:

typescript
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 detailType and eventSource
  • 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.ts

See Also ​