1---
2title: Build a release-automation agent
3description: Case study of the cloudflare-agent example — GitHub push events routed through a Worker to a per-release Durable Object that drives an AI agent with container-backed tools.
4sidebar:
5 order: 27
6---
7
8The [`cloudflare-agent` example](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-agent)
9is an autonomous release pipeline: a push lands on `main`, a Worker
10filters for release commits, and each one is handed to a Durable
11Object that drives an AI agent to write the release blog post — with
12file, shell, and SQL tools that execute inside a Cloudflare
13Container. This guide walks the architecture; the example source is
14the full listing.
15
16The pipeline is four pieces, each one file:
17
18- `src/ReleaseService.ts` — the Worker that consumes push events
19- `src/ReleaseVersion.ts` — a Durable Object per release commit
20- `src/ReleaseBlogger.ts` — the agent: a prompt that references its tools
21- `src/DevBox.ts` — the container the tools execute in
22
23## The front door: push events
24
25`ReleaseService` subscribes to the repository's `push` webhook with
26[`GitHub.consumeRepositoryEvents`](/cloudflare/messaging/github-events)
27and filters for release commits on `main` in ordinary handler code:
28
29```typescript
30// src/ReleaseService.ts
31yield* Github.consumeRepositoryEvents(
32 { owner: "alchemy-run", repository: "alchemy", events: ["push"] },
33 (event) => {
34 const title = event.payload.head_commit?.message.split("\n")[0] ?? "";
35 const isRelease =
36 event.payload.ref === "refs/heads/main" &&
37 title.startsWith("chore(release):");
38
39 return isRelease
40 ? versions.getByName(event.payload.head_commit!.id).generateBlog({
41 input: event.payload,
42 })
43 : Effect.log(`Skipping "${event.payload.head_commit?.message}"`);
44 },
45);
46```
47
48At deploy time alchemy provisions the webhook on the repo pointing at
49this Worker and binds the signing secret; at runtime it verifies each
50delivery and hands the handler a typed `push` payload — the filter is
51just a string check on the commit title.
52
53## One Durable Object per release
54
55`versions.getByName(commitId)` routes each release to a
56[Durable Object](/cloudflare/compute/durable-objects) keyed by the commit
57hash, so webhook redeliveries for the same commit land on the same
58instance instead of kicking off duplicate work:
59
60```typescript
61// src/ReleaseVersion.ts
62export class ReleaseVersion extends Cloudflare.DurableObject<ReleaseVersion>()(
63 "ReleaseBlogger",
64 Effect.gen(function* () {
65 const blogger = yield* ReleaseBlogger;
66 return Effect.gen(function* () {
67 return {
68 generateBlog: Effect.fn(function* (request: { input: any }) {
69 yield* blogger.send(request);
70 }),
71 };
72 });
73 }).pipe(Effect.provide(/* tool + container layers, below */)),
74) {}
75```
76
77The object body is tiny — resolve the agent, forward the payload. All
78the capability wiring lives in the layer stack it's provided with.
79
80## The agent: a prompt that references its tools
81
82`ReleaseBlogger` is an `Alchemy.Agent` — a tagged-template prompt
83that interpolates the tool classes it may call. Referencing a tool in
84the template adds it to the agent's inferred requirements, so the
85type system knows exactly which layers the host must provide:
86
87```typescript
88// src/ReleaseBlogger.ts
89export class ReleaseBlogger extends Alchemy.Agent<ReleaseBlogger>()("Blogger")`
90You are the Release Blogger. Your job is to turn a merged pull request into a
91release blog post under website/src/content/docs/blog/.
92
931. Use the ${Grep} tool to find the most recent post...
942. Use the ${ReadFile} tool to read that post and the PR diff...
954. Use the ${WriteFile} tool to create the new post...
96` {}
97```
98
99Each tool is an `AI.Tool` — an Effect service with schema-typed
100parameters (`AI.Parameter`) — so implementations are swappable
101layers. `src/tools/Fs.ts` ships two `WriteFile` implementations:
102`WriteFileR2` puts files in an R2 bucket, `WriteFileDevBox` writes to
103the container's filesystem. The DO picks one at layer-composition
104time; the prompt doesn't change.
105
106:::note
107`Alchemy.Agent` is the newest surface in this example — the
108declarative API (prompt + tool refs + inferred requirements) is what
109the example demonstrates, and its runtime is still landing. Treat the
110example as the reference for the architecture, not a turnkey deploy.
111:::
112
113## The DevBox: a container as tool backend
114
115Shell and filesystem tools need a real machine. `DevBox` is a
116[Cloudflare Container](/cloudflare/compute/run-a-container) with a typed
117RPC interface, implemented with Effect's `FileSystem` and
118`ChildProcessSpawner` running inside the container:
119
120```typescript
121// src/DevBox.ts
122export class DevBox extends Cloudflare.Container<
123 DevBox,
124 {
125 readFile: (path: string) => Effect.Effect<string>;
126 writeFile: (path: string, contents: string) => Effect.Effect<void>;
127 exec: (command: string) => Effect.Effect<{
128 exitCode: number;
129 stdout: string;
130 stderr: string;
131 }>;
132 }
133>()("DevBox") {}
134
135export default DevBox.make(
136 { main: import.meta.url, image: "oven/bun:1.3" },
137 Effect.gen(function* () {
138 /* FileSystem + ChildProcessSpawner impl */
139 }),
140);
141```
142
143The `ReleaseVersion` DO provides
144`Cloudflare.Containers.layer(DevBox, { enableInternet: true })`, so
145tool layers like `WriteFileDevBox` resolve the container and call its
146typed methods directly. The other tools follow the same pattern with
147different backends: `Sql` runs against the DO's own SQLite storage,
148and `Eval` sandboxes generated JavaScript in a dynamically loaded
149Worker via `Cloudflare.WorkerLoader`.
150
151## The Stack
152
153`alchemy.run.ts` ties it together — GitHub providers for the webhook,
154Cloudflare providers for everything else, and the DevBox layer
155provided at the top:
156
157```typescript
158// alchemy.run.ts
159export default Alchemy.Stack(
160 "Stack",
161 {
162 providers: Layer.mergeAll(Cloudflare.providers(), GitHub.providers()),
163 state: Cloudflare.state(),
164 },
165 Effect.gen(function* () {
166 const releaseService = yield* ReleaseService;
167 return { releaseService: releaseService.url.as<string>() };
168 }).pipe(Effect.provide(DevBoxLive)),
169);
170```
171
172One `bun alchemy deploy` provisions the Worker, the Durable Object,
173the container image, and the GitHub webhook — the whole pipeline is a
174single typed program.
175
176## Where next
177
178- [React to GitHub events](/cloudflare/messaging/github-events) — the
179 event source in depth: provisioning, signatures, typed payloads.
180- [Workflows](/cloudflare/compute/workflows) — durable multi-step
181 execution if a release pipeline outgrows a single DO method.
182- [Containers](/cloudflare/compute/run-a-container) — the DevBox pattern:
183 typed RPC into a Docker image.
184- [GitHub provider](/github) — repositories, Actions config, and the
185 rest of the CI/CD glue.