1---
2title: 2.0.0-beta.61 - Workers Cache, Routes & Sync
3date: 2026-07-08T06:00:00Z
4category: release
5excerpt: v2.0.0-beta.61 adds Workers Cache with an Effect-native ExecutionContext, Wrangler-style zone routes, alchemy sync for repairing state drift, a full Workflows API (retries, rollback, events), resource type aliases, and a long list of reliability fixes.
6---
7
8beta.61 adds **Workers Cache** with an Effect-native
9`ExecutionContext`, Wrangler-style **zone routes**, **`alchemy
10sync`** for repairing out-of-band drift, a much fuller
11**Workflows** API (retries, rollback, events), **resource type
12aliases** that heal the beta.59 renames, and a long list of
13reliability fixes.
14
15:::caution
16**Breaking changes** — migration for each is below.
17
18- **`workflow.create` takes the native options object** —
19 `create(input)` → `create({ params: input })`
20 ([#611](https://github.com/alchemy-run/alchemy/pull/611)).
21- **`WorkerExecutionContext` is now an Effect wrapper** — it's no
22 longer the raw `cf.ExecutionContext`; `waitUntil` takes an
23 `Effect` and is `yield*`-ed
24 ([#752](https://github.com/alchemy-run/alchemy/pull/752)).
25- **effect `>=4.0.0-beta.93` is required** — effect moved
26 `UrlParams.makeUrl` to `Url.make`; the peer range floor is raised
27 to match
28 ([#748](https://github.com/alchemy-run/alchemy/pull/748)).
29:::
30
31## Workers Cache
32
33[Workers Cache](https://blog.cloudflare.com/workers-cache/) —
34Cloudflare's regionally tiered cache in front of Worker
35entrypoints — lands as both a binding and a prop
36([#752](https://github.com/alchemy-run/alchemy/pull/752)).
37Effect-native Workers enable it with `Cloudflare.cache()`, which
38also returns the typed purge client:
39
40```typescript
41Effect.gen(function* () {
42 // init: enables Workers Cache on this Worker at deploy time
43 const { purge } = yield* Cloudflare.cache({ crossVersionCache: true });
44
45 return {
46 fetch: Effect.gen(function* () {
47 const request = yield* HttpServerRequest;
48 if (request.url.startsWith("/invalidate")) {
49 yield* purge({ tags: ["products"] }); // typed CachePurgeError
50 return HttpServerResponse.text("purged");
51 }
52 return HttpServerResponse.text("hello", {
53 headers: {
54 "Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
55 "Cache-Tag": "products",
56 },
57 });
58 }),
59 };
60})
61```
62
63Async Workers use the prop:
64
65```typescript
66const worker = yield* Cloudflare.Worker("Api", {
67 main: "./src/api.ts",
68 cache: { enabled: true, crossVersionCache: true },
69});
70```
71
72## Effect-native `ExecutionContext`
73
74`WorkerExecutionContext` used to hand you the raw
75`cf.ExecutionContext`. It's now an Effect wrapper — and like
76`DurableObjectState`, it can be yielded from the Worker's **init
77closure** (or any Layer); its methods resolve the live per-event
78context:
79
80```typescript
81Effect.gen(function* () {
82 const exec = yield* Cloudflare.WorkerExecutionContext; // init
83
84 return {
85 fetch: Effect.gen(function* () {
86 // respond now, finish work in the background
87 yield* exec.waitUntil(journal.record(entry).pipe(Effect.delay("5 seconds")));
88 return HttpServerResponse.text("ok");
89 }),
90 };
91})
92```
93
94Migrating is mechanical — `ctx.waitUntil(promise)` becomes
95`yield* exec.waitUntil(effect)`.
96
97## Zone Routes on Workers
98
99`Cloudflare.Worker` accepts Wrangler-style zone routes via a new
100`routes` prop
101([#438](https://github.com/alchemy-run/alchemy/pull/438)).
102Thanks **utopy**!
103
104```typescript
105yield* Cloudflare.Worker("Api", {
106 main: import.meta.filename,
107 routes: [
108 { pattern: "api.example.com/*", zoneName: "example.com" },
109 { pattern: "example.com/api/*", zoneId: "<YOUR_ZONE_ID>" },
110 ],
111});
112```
113
114Each entry takes `zoneName` / `zoneId` (the Wrangler equivalents)
115or a `zone` reference — and when the zone is omitted, it's inferred
116from the pattern hostname. Routes are reconciled on deploy and
117cleaned up on destroy.
118
119## `alchemy sync`
120
121Cloud state drifts: someone edits a resource in the dashboard, a
122bucket gets deleted, tags get mangled. `alchemy sync` converges it
123back to the last-deployed state — without re-running your stack
124program
125([#766](https://github.com/alchemy-run/alchemy/pull/766)):
126
127```sh
128alchemy sync ./alchemy.run.ts --stage prod # detect + repair
129alchemy sync ./alchemy.run.ts --stage prod --dry-run # detect only
130```
131
132Per resource it runs observe → compare → converge: `read` observes
133the live cloud state, a deep-compare against the persisted
134attributes decides `unchanged` vs drifted, and drift is repaired by
135`reconcile` with the persisted props as the desired state. A
136resource deleted out-of-band is recreated under the **same
137instance id**, so deterministic physical names regenerate
138identically. Resources sync concurrently, and failures are
139aggregated after every resource has been attempted.
140
141## Workflows: retries, rollback, and events
142
143The Effect-native Workflow wrapper now covers the full Workers API
144surface, 1:1 with the native binding
145([#611](https://github.com/alchemy-run/alchemy/pull/611)).
146Thanks **Gerben Mulder**!
147
148`create` takes the native options object — the breaking change
149above — which also unlocks `id` and `retention`:
150
151```diff lang="typescript"
152- const instance = yield* workflow.create({ orderId: "abc" });
153+ const instance = yield* workflow.create({
154+ id: "order-abc",
155+ params: { orderId: "abc" },
156+ retention: { successRetention: "1 day", errorRetention: "7 days" },
157+ });
158```
159
160`task` gains retries, timeout, and rollback, and
161`WorkflowStepContext` exposes the current attempt:
162
163```typescript
164const result = yield* Cloudflare.Workflows.task(
165 "call-api",
166 Effect.gen(function* () {
167 const context = yield* Cloudflare.Workflows.WorkflowStepContext;
168 return { attempt: context.attempt };
169 }),
170 {
171 retries: { limit: 3, delay: "5 seconds", backoff: "linear" },
172 rollback: ({ output }) => (output ? cleanup(output.id) : Effect.void),
173 },
174);
175```
176
177`waitForEvent` parks the instance until a matching `sendEvent`:
178
179```typescript
180// inside the workflow
181const approval = yield* Cloudflare.Workflows.waitForEvent<{ approved: boolean }>(
182 "approval",
183 { type: "approval", timeout: "1 day" },
184);
185
186// from outside
187yield* instance.sendEvent({ type: "approval", payload: { approved: true } });
188```
189
190`createBatch`, `restart`, rollback status, and extended event
191metadata round out the surface. Docs:
192[Workflows](/cloudflare/compute/workflows).
193
194## Resource type aliases
195
196Renaming a resource's type string used to strand state persisted
197under the old name — provider lookup died on the legacy type.
198Resources now declare their former names
199([#765](https://github.com/alchemy-run/alchemy/pull/765)):
200
201```typescript
202export const Queue = Resource<Queue>("Cloudflare.Queues.Queue", {
203 aliases: ["Cloudflare.Queue"],
204});
205```
206
207Plan, apply, destroy, `logs`, and `tail` all resolve providers
208through aliases, and a noop deploy migrates state rows to the
209canonical name. All **74 resources** renamed in the beta.59
210namespace alignment are annotated with their pre-rename alias — so
211state written by beta.58 or earlier now deploys, destroys, and
212replaces cleanly instead of erroring on the legacy type.
213
214## AI Gateway BYOK: `AI.ProviderKey`
215
216A bring-your-own-key provider on an AI Gateway needs two coordinated
217resources — a Secrets Store `Secret` with an exact
218`{gatewayId}_{providerSlug}_{alias}` name, and a `GatewayProvider`
219that references it. `Cloudflare.AI.ProviderKey` owns that contract
220as one resource
221([#586](https://github.com/alchemy-run/alchemy/pull/586)).
222Thanks **Alex**!
223
224```typescript
225const { secret, gatewayProvider } = yield* Cloudflare.AI.ProviderKey("OpenAiKey", {
226 store,
227 gatewayId: gateway.gatewayId,
228 providerSlug: "openai",
229 value: yield* Config.redacted("OPENAI_API_KEY"),
230});
231```
232
233Docs: [AI Gateway](/cloudflare/ai/ai-gateway).
234
235## Lambda async invocation config
236
237Lambda's asynchronous invocation settings — retries, event age,
238success/failure destinations — land as an `eventInvokeConfig` prop
239on `Function` and `Alias`
240([#627](https://github.com/alchemy-run/alchemy/pull/627)).
241Thanks **José Netto**!
242
243```typescript
244const fn = yield* AWS.Lambda.Function("AsyncFn", {
245 main: "./src/handler.ts",
246 eventInvokeConfig: {
247 maximumRetryAttempts: 0,
248 maximumEventAgeInSeconds: 60,
249 destinationConfig: {
250 OnFailure: { Destination: queue.queueArn },
251 },
252 },
253});
254```
255
256## R2 Bucket `cors`
257
258The `cors` prop that `Cloudflare.R2.Bucket` had in v1 is restored
259([#771](https://github.com/alchemy-run/alchemy/pull/771)) —
260public buckets serving browser range-reads (e.g. PMTiles) declare
261CORS in the stack again instead of out-of-band:
262
263```typescript
264const bucket = yield* Cloudflare.R2.Bucket("Tiles", {
265 domains: [{ name: "tiles.example.com" }],
266 cors: [
267 {
268 allowedMethods: ["GET", "HEAD"],
269 allowedOrigins: ["https://map.example.com"],
270 allowedHeaders: ["range"],
271 exposeHeaders: ["etag", "content-range"],
272 maxAgeSeconds: 3600,
273 },
274 ],
275});
276```
277
278Rules use the flat S3-style shape and reconcile like
279`lifecycleRules` — observed vs desired, so out-of-band drift and
280adoption converge correctly.
281
282## Also in this release
283
284- **Worker metadata-only changes now deploy**
285 ([#747](https://github.com/alchemy-run/alchemy/pull/747)) —
286 compatibility flags/date, observability, placement, limits, and
287 binding changes are folded into the update diff via a metadata
288 hash; previously they planned as noops and silently never
289 shipped. The first deploy after upgrading plans a one-time update
290 per Worker to backfill the hash. Thanks **Alex**!
291- **Wedged stacks recover**
292 ([#767](https://github.com/alchemy-run/alchemy/pull/767),
293 [#770](https://github.com/alchemy-run/alchemy/pull/770)) —
294 a deploy interrupted mid-create used to persist a state row whose
295 Output-valued props can't round-trip, crashing every subsequent
296 `plan`/`deploy`/`destroy`. Every provider's `read`/`diff` is
297 audited so the engine re-drives the create and reconcile
298 converges on the half-created resource.
299- **Script uploads retry every binding-target-not-found error**
300 ([#753](https://github.com/alchemy-run/alchemy/pull/753)) —
301 KV, R2, D1, Queues, DO classes, Hyperdrive, Vectorize, and more,
302 covering Cloudflare's deploy-time propagation lag.
303- **`Resource.ref` values bind natively in Worker `env`**
304 ([#756](https://github.com/alchemy-run/alchemy/pull/756)) —
305 a ref passed as an env binding used to degrade to a plain JSON
306 env var and break at runtime; refs now classify exactly like
307 locally-declared resources.
308- **Multiple queue consumers on one Worker**
309 ([#466](https://github.com/alchemy-run/alchemy/pull/466)) —
310 event dispatch runs every listener for an event type instead of
311 letting the first queue subscription swallow the rest. Thanks
312 **Leonardo E. Dominguez**!
313- **State-store errors are actionable**
314 ([#737](https://github.com/alchemy-run/alchemy/pull/737)) —
315 30 days of production traces triaged; empty `StateStoreError:`
316 messages, opaque decode errors, and JSON-parse crashes now
317 surface real messages (an unauthorized store tells you to run
318 `alchemy login`).
319- **The test suite passes on Windows**
320 ([#735](https://github.com/alchemy-run/alchemy/pull/735)) —
321 including two real product fixes: `Drizzle.Schema` passed
322 OS-native paths to drizzle-kit (backslashes are glob escapes),
323 and `Bundle.PurePlugin` could be hijacked by a stray
324 `package.json` above `node_modules`.
325- **Drizzle query chains are real Effects**
326 ([#750](https://github.com/alchemy-run/alchemy/pull/750)) —
327 `db.select()...` chains now expose the full Effect protocol, so
328 they compose with `Effect.all` and friends instead of spinning
329 the run loop.
330- **PlanetScale inherited roles compare by membership**
331 ([#761](https://github.com/alchemy-run/alchemy/pull/761)) —
332 API-returned ordering no longer forces a `PostgresRole`
333 replacement. Thanks **Gerben Mulder**!
334- **`globalOutbound: null` is preserved in `WorkerLoader`**
335 ([#746](https://github.com/alchemy-run/alchemy/pull/746)) —
336 the documented "block all outbound network access" signal no
337 longer silently coerces to default access. Thanks **Alex**!
338- **Binding-hosted DO classes join the precreate stub**
339 ([#764](https://github.com/alchemy-run/alchemy/pull/764)) —
340 fixes `Worker did not expose Durable Object namespace` on fresh
341 deploys of worker↔container cycles. Thanks **Daniel Gangl**!
342- **`alchemy dev` no longer prints bun's benign tsconfig fd
343 warning**
344 ([#768](https://github.com/alchemy-run/alchemy/pull/768)).
345
346## Where to go next
347
348- [Workflows](/cloudflare/compute/workflows)
349- [Workers](/cloudflare/compute/workers)
350- [Custom Domains & Routes](/cloudflare/networking/custom-domains)
351- [AI Gateway](/cloudflare/ai/ai-gateway)
352- [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta61)
353- [Compare v2.0.0-beta.60 → v2.0.0-beta.61](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.60...v2.0.0-beta.61)