Skip to content

EventBridge & Scheduler

EventBridge routes events. A producer puts an event on a bus, and rules on the bus look at its content and send it to whichever targets match. Producers don’t know the targets, and targets don’t know each other, so new reactions to an event are added as new rules.

AWS services publish their own events to every account’s default bus: an object created, an instance stopped, a task that failed. EventBridge Scheduler is the same idea driven by time: it invokes a target on a rate, a cron expression or a single date.

Every event has a source, a detail-type and a JSON detail. A rule’s event pattern lists the values it matches, and a matching event goes to the rule’s targets:

Orders · event busevent"source": "shop.orders""detail-type": "OrderPlaced""detail": { "total": 120 }rule{ "detail-type": ["OrderPlaced"] }WorkerLambda Functionrule{ "source": ["shop.orders"] }AuditSQS Queuerule{ "detail-type": ["OrderRefunded"] }no matchRefundsLambda Function
Each rule on the bus tests the event against its pattern. This OrderPlaced event matches two rules, so both of their targets get it, and the third rule ignores it.

A pattern can match on any field of the event, including fields inside detail, so a rule can take only the orders over a certain amount, or only the ones from one region. Events that match no rule are dropped.

src/bus.ts
import * as AWS from "@alchemy.run/aws";
export const Orders = AWS.EventBridge.EventBus("Orders", {});

Your own events go on a bus of your own. The account’s default bus, where AWS services publish, needs no declaration: leave the bus out of the calls below to use it.

Bind EventBridge.PutEvents to the bus. Its role gets events:PutEvents on that bus only:

src/api.ts
const bus = yield* Orders;
const putEvents = yield* AWS.EventBridge.PutEvents(bus);
yield* putEvents({
Entries: [{ Source: "app.orders", DetailType: "OrderPlaced", Detail: JSON.stringify({ orderId }) }],
});

Provide AWS.EventBridge.PutEventsHttp to the runtime. The bus name is filled in from the binding.

Subscribe a Lambda Function to a pattern from Construction. Matching events arrive as a Stream:

src/worker.ts
export default class Worker extends AWS.Lambda.Function<Worker>()(
"Worker",
{ main: import.meta.url, functionUrl: false },
Effect.gen(function* () {
const bus = yield* Orders;
yield* AWS.EventBridge.consumeBusEvents(
bus,
{ source: ["app.orders"], "detail-type": ["OrderPlaced"] },
(events) => Stream.runForEach(events, (event) => Effect.log(event.detail)),
);
return {};
}).pipe(Effect.provide(AWS.Lambda.EventSource)),
) {}

Deploy creates the rule and the Lambda permission that lets EventBridge invoke the Function. Each event’s detail is unknown until you narrow the stream to the type you expect, AWS.EventBridge.EventRecord<OrderPlaced>.

Some targets need no handler of your own: a Queue to buffer events, an ECS task to process them. EventBridge.events builds the rule and sends matches straight to the target:

alchemy.run.ts
yield* AWS.EventBridge.events(bus, { source: ["app.orders"] }).toQueue(audit);

.toQueue adds a statement to the Queue’s policy letting this rule send to it. .toLambda adds the invoke permission, and .toEcsTask creates a role that may run the task. Each grant is limited to the one rule.

For input transformers, retry policies, dead-letter queues or several targets on one rule, declare an AWS.EventBridge.Rule yourself. A raw rule grants nothing, so give its targets a role or a resource policy.

AWS services publish to the default bus whenever something happens in your account. Match their source to react to them, leaving the bus out:

src/worker.ts
yield* AWS.EventBridge.consumeBusEvents(
{ source: ["aws.ecs"], "detail-type": ["ECS Task State Change"] },
(events) => Stream.runForEach(events, (event) => Effect.log(event.detail)),
);

This is how you watch for failed tasks, stopped instances or new objects without polling anything.

EventBridge Scheduler invokes a target on a schedule. Scheduler.every takes a plain duration:

alchemy.run.ts
const report = yield* Report;
yield* AWS.Scheduler.every("5 minutes").toLambda(report);

The schedule needs a role that Scheduler can assume and that may invoke the Function. The builder creates it, with lambda:InvokeFunction on that Function only.

For a time of day, use cron with a timezone, and pass an input:

alchemy.run.ts
yield* AWS.Scheduler.cron("cron(0 3 * * ? *)", { timezone: "America/New_York" })
.named("nightly-report")
.toLambda(report, { input: { job: "nightly-report" } });

For a single moment, use at. Schedules can target Queues too:

alchemy.run.ts
yield* AWS.Scheduler.at(new Date("2026-08-01T09:00:00Z")).toQueue(reminders, { type: "launch" });

A job that runs too long for Lambda runs as an ECS task on a schedule instead:

alchemy.run.ts
yield* AWS.ECS.every("NightlyJob", "cron(0 3 * * ? *)", {
cluster,
task: nightlyTask,
subnets: network.privateSubnetIds,
securityGroups: [jobSecurityGroup.groupId],
});

The schedule gets a role that may run the task and pass its roles. See ECS.

Use EventBridge when many kinds of events share one bus and different parts of the app care about different ones, or when you react to AWS’s own events and schedules. Use an SNS Topic for one channel whose every message goes to every subscriber. Use an SQS Queue to buffer work for one consumer.