1---
2title: EventBridge & Scheduler
3description: Route application and AWS events through EventBridge buses and rules, consume them in Lambda as typed streams, and run cron/rate schedules against Lambda, SQS, and ECS with EventBridge Scheduler.
4---
5
6**EventBridge** is AWS's event router — producers publish events onto
7a bus, rules match them by pattern, and targets receive them.
8**EventBridge Scheduler** is its time-based sibling: fire an
9invocation on a `rate(...)`, `cron(...)`, or one-time `at(...)`
10expression. In Alchemy the surface is:
11
12- `EventBridge.EventBus` and `EventBridge.Rule` — the raw resources.
13- `EventBridge.PutEvents` — a producer binding that grants
14 `events:PutEvents` on the bus you publish to.
15- `EventBridge.consumeBusEvents` — subscribe a Lambda to a pattern as
16 a typed `Stream`, creating the rule and invoke permission for you.
17- `EventBridge.events(bus, pattern).toLambda/.toQueue/.toEcsTask` —
18 route matching events to a target with no handler code.
19- `Scheduler.every/cron/at` — chainable schedule builders that
20 synthesize the target's IAM role.
21- `ECS.every` — run a Fargate task on a schedule.
22
23## Create an event bus
24
25An event bus is a one-line resource. Declare it in a shared module so
26producers and consumers can reference the same handle:
27
28```typescript
29// src/bus.ts
30import * as AWS from "alchemy/AWS";
31
32export const Events = AWS.EventBridge.EventBus("Events", {});
33```
34
35No name required — Alchemy generates one from the app, stage, and
36logical ID. The name `default` is reserved: to target the account's
37**default bus** (where AWS service events land), simply omit the bus
38argument in the APIs below.
39
40## Publish events from a Lambda
41
42`EventBridge.PutEvents(bus)` returns a callable Effect and quietly
43attaches an `events:PutEvents` policy statement scoped to the bus ARN
44(or the default bus, if you pass nothing):
45
46```typescript
47// src/api.ts
48import * as AWS from "alchemy/AWS";
49import * as Effect from "effect/Effect";
50import * as HttpServerResponse from "effect/http/HttpServerResponse";
51import { Events } from "./bus.ts";
52
53export default class Api extends AWS.Lambda.Function<Api>()(
54 "Api",
55 { main: import.meta.url, functionUrl: true },
56 Effect.gen(function* () {
57 const bus = yield* Events;
58 const putEvents = yield* AWS.EventBridge.PutEvents(bus);
59
60 return {
61 fetch: Effect.gen(function* () {
62 yield* putEvents({
63 Entries: [
64 {
65 Source: "app.orders",
66 DetailType: "OrderPlaced",
67 Detail: JSON.stringify({ orderId: "o-1" }),
68 },
69 ],
70 }).pipe(Effect.orDie);
71 return HttpServerResponse.empty({ status: 202 });
72 }),
73 };
74 }).pipe(Effect.provide(AWS.EventBridge.PutEventsHttp)),
75) {}
76```
77
78Entries carry a `Source`, a `DetailType`, and a JSON-encoded `Detail`
79— the three fields rules match on. The `EventBusName` is filled in
80from the bus you bound, so entries never repeat it.
81`AWS.EventBridge.PutEventsHttp` is the runtime layer that implements
82the binding over the EventBridge API.
83
84## Consume events in a Lambda
85
86`consumeBusEvents` is the consumer-side mirror of
87`SQS.consumeQueueMessages`: pass the bus, an event pattern, and a
88handler that receives matching events as a typed `Stream`:
89
90```typescript
91// src/worker.ts
92import * as AWS from "alchemy/AWS";
93import * as Effect from "effect/Effect";
94import * as Stream from "effect/Stream";
95import { Events } from "./bus.ts";
96
97export default class Worker extends AWS.Lambda.Function<Worker>()(
98 "OrdersWorker",
99 { main: import.meta.url },
100 Effect.gen(function* () {
101 const bus = yield* Events;
102
103 yield* AWS.EventBridge.consumeBusEvents(
104 bus,
105 { source: ["app.orders"], "detail-type": ["OrderPlaced"] },
106 (events) =>
107 Stream.runForEach(events, (event) =>
108 Effect.log(`order placed: ${JSON.stringify(event.detail)}`),
109 ),
110 );
111
112 return {};
113 }).pipe(Effect.provide(AWS.Lambda.EventSource)),
114) {}
115```
116
117That single call does the infrastructure work at deploy time — an
118EventBridge `Rule` with the pattern targeting this function, plus the
119Lambda permission that lets `events.amazonaws.com` invoke it — and at
120runtime filters incoming invocations against the same pattern before
121feeding them into your stream. Omit the `bus` argument to subscribe to
122the default bus, which is where AWS service events (S3, EC2, ECS
123state changes, …) arrive.
124
125`AWS.Lambda.EventSource` is the Lambda runtime implementation of the
126EventBridge event source — provide it at the bottom of the function
127like any other event source layer.
128
129### Type the event detail
130
131Events are `EventRecord<Detail>` with `Detail` defaulting to
132`unknown`. Narrow the stream when you know the payload shape:
133
134```typescript
135type OrderPlaced = { orderId: string };
136
137yield* AWS.EventBridge.consumeBusEvents(
138 bus,
139 { source: ["app.orders"], "detail-type": ["OrderPlaced"] },
140 (events) =>
141 Stream.runForEach(
142 events as Stream.Stream<AWS.EventBridge.EventRecord<OrderPlaced>>,
143 (event) => Effect.log(`order: ${event.detail.orderId}`),
144 ),
145);
146```
147
148## Route events without a handler
149
150Not every consumer is code you own. `EventBridge.events(bus, pattern)`
151builds a route — chain `.toLambda`, `.toQueue`, or `.toEcsTask` to
152deliver matching events to a target resource, permissions included:
153
154```typescript
155// alchemy.run.ts
156yield* AWS.EventBridge.events(bus, { source: ["app.orders"] })
157 .toQueue(auditQueue);
158```
159
160Each target wires its own access: `.toQueue` attaches a queue policy
161allowing `events.amazonaws.com` to `sqs:SendMessage` (conditioned on
162the rule's ARN), `.toLambda` adds the invoke permission, and
163`.toEcsTask` creates an IAM role with `ecs:RunTask` and `iam:PassRole`
164for the task's roles. Pass a string as the first argument to name the
165backing rule deterministically instead of deriving it from the
166pattern hash.
167
168## Rules directly
169
170The helpers above all bottom out in the `Rule` resource. Reach for it
171directly when you need full control — input transformers, dead-letter
172queues, retry policies, or up to five targets per rule:
173
174```typescript
175const rule = yield* AWS.EventBridge.Rule("ObjectCreated", {
176 eventPattern: {
177 source: ["aws.s3"],
178 "detail-type": ["Object Created"],
179 },
180 targets: [{ Id: "Jobs", Arn: queue.queueArn }],
181});
182```
183
184A rule needs an `eventPattern` or a `scheduleExpression` (rules also
185accept `rate(...)`/`cron(...)` expressions, though Scheduler below is
186the better tool for time-based work). Omitting `eventBusName` puts
187the rule on the default bus. Unlike the `events(...)` helpers, a raw
188`Rule` does **not** grant the target-side permissions — supply a
189target `RoleArn` or resource policy yourself.
190
191## Scheduled jobs with Scheduler
192
193`AWS.Scheduler` exposes chainable builders. The simplest form invokes
194a Lambda on a fixed rate:
195
196```typescript
197// alchemy.run.ts
198const api = yield* Api;
199
200yield* AWS.Scheduler.every("5 minutes").toLambda(api);
201```
202
203`every` accepts a plain-English duration (normalized to
204`rate(5 minutes)`) or a full `rate(...)`/`cron(...)` expression
205as-is. The builder synthesizes the piece Scheduler always demands —
206an execution role assumable by `scheduler.amazonaws.com` with
207`lambda:InvokeFunction` on exactly that function — then creates the
208`Scheduler.Schedule` resource.
209
210Cron schedules take an options object with a timezone, and `.named`
211pins the schedule's name:
212
213```typescript
214yield* AWS.Scheduler
215 .cron("cron(0 3 * * ? *)", { timezone: "America/New_York" })
216 .named("nightly-report")
217 .toLambda(api, { input: { job: "nightly-report" } });
218```
219
220`input` is delivered as the invocation payload (objects are
221JSON-stringified for you); `retryPolicy` and `deadLetterConfig` are
222also accepted per target.
223
224One-time schedules use `at(date)`, and `.toQueue` sends a payload to
225SQS instead of invoking a function:
226
227```typescript
228yield* AWS.Scheduler
229 .at(new Date("2026-08-01T09:00:00Z"))
230 .toQueue(queue, { type: "launch.reminder" });
231```
232
233Builder options cover the rest of the Scheduler feature set —
234`group` (a `Scheduler.ScheduleGroup`), `startDate`/`endDate`,
235`flexibleTimeWindow`, `state`, `kmsKeyArn`, and
236`actionAfterCompletion` for one-time schedules.
237
238### The Schedule resource directly
239
240The builders materialize `Scheduler.Schedule` under the hood. Use it
241directly when you bring your own role:
242
243```typescript
244const schedule = yield* AWS.Scheduler.Schedule("Poll", {
245 scheduleExpression: "rate(1 hour)",
246 flexibleTimeWindow: { Mode: "OFF" },
247 target: {
248 Arn: queue.queueArn,
249 RoleArn: role.roleArn,
250 },
251});
252```
253
254The `RoleArn` must trust `scheduler.amazonaws.com`. Alchemy retries
255the creation while the freshly-created IAM role propagates, so a role
256declared in the same deploy just works.
257
258## ECS scheduled tasks
259
260For jobs too heavy for Lambda, `AWS.ECS.every` runs a Fargate task on
261a schedule — cron-as-a-container:
262
263```typescript
264yield* AWS.ECS.every("HourlyJob", "1 hour", {
265 cluster,
266 task: jobTask,
267 subnets: [privateSubnet1.subnetId, privateSubnet2.subnetId],
268 securityGroups: [jobSecurityGroup.groupId],
269});
270```
271
272`cluster` and `task` are the outputs of `AWS.ECS.Cluster` and
273`AWS.ECS.Task` (see [ECS](/aws/compute/ecs) for standing those up). Under the
274hood this is `Scheduler.every(...).named("HourlyJob").toEcsTask(...)`:
275a schedule plus an execution role with `ecs:RunTask` on the task
276definition and `iam:PassRole` for its task and execution roles. Plain
277durations normalize to `rate(...)`; full `cron(...)` expressions pass
278through unchanged:
279
280```typescript
281yield* AWS.ECS.every("NightlyJob", "cron(0 3 * * ? *)", {
282 cluster,
283 task: nightlyTask,
284 subnets: [privateSubnet1.subnetId, privateSubnet2.subnetId],
285 securityGroups: [jobSecurityGroup.groupId],
286 taskCount: 3,
287 input: JSON.stringify({ source: "scheduler" }),
288});
289```
290
291Tasks launch as Fargate with no public IP by default
292(`assignPublicIp: true` to change that), `taskCount` copies per tick,
293and an optional static `input` forwarded to the task.
294
295## Where next
296
297- [Lambda](/aws/compute/lambda) — the runtime both the consumer and scheduled
298 targets above deploy to.
299- [SNS](/aws/messaging/sns) — pub/sub fan-out when you want subscriptions
300 rather than pattern-matching rules.
301- [ECS](/aws/compute/ecs) — the cluster, task, and networking that
302 `ECS.every` schedules onto.
303- [`EventBus`](/providers/aws/eventbridge/eventbus),
304 [`Rule`](/providers/aws/eventbridge/rule), and
305 [`Schedule`](/providers/aws/scheduler/schedule) references — every
306 prop and attribute.