| 1 | --- |
| 2 | title: 2.0.0-beta.77 - Profiles, CLI & Workers Tracing |
| 3 | date: 2026-09-08T09:00:00Z |
| 4 | category: release |
| 5 | excerpt: v2.0.0-beta.77 redesigns the CLI — alchemy login becomes alchemy profile, profiles are stored as a folder per profile with a file per provider, and every TUI action has a non-interactive twin for agents and CI — and adds Cloudflare.Telemetry() for tracing Workers in Workers Observability. Also dependency-aware dev reloads, cron schedules on Workflows, provisioned ElastiCache, and private/pre-deploy/local-context Railway services. |
| 6 | --- |
| 7 | |
| 8 | beta.77 redesigns the CLI. `alchemy login` becomes |
| 9 | **`alchemy profile`**, a dashboard for every connected account |
| 10 | whose every action is also a non-interactive subcommand; profiles |
| 11 | are stored as **a folder per profile with a file per provider** |
| 12 | instead of a central `profiles.json`; and `alchemy dev` gets a |
| 13 | plan/output widget you can flip with `←/→`. Outside the CLI, |
| 14 | **`Cloudflare.Telemetry()`** sends Effect spans to Workers |
| 15 | Observability with no OTLP setup, Workflows take cron schedules, |
| 16 | and AWS gains provisioned ElastiCache. |
| 17 | |
| 18 | Here is the new CLI on a fresh machine, start to finish: |
| 19 | |
| 20 | <video controls muted playsinline preload="metadata" poster="/blog/beta-77/alchemy-cli-poster.png" src="/blog/beta-77/alchemy-cli.mp4" style="width: 100%; border-radius: 8px;"></video> |
| 21 | |
| 22 | _`alchemy profile` → `alchemy dev` → `alchemy deploy` → `alchemy drift` → `alchemy destroy`. The same walkthrough, frame by frame, is below._ |
| 23 | |
| 24 | :::caution |
| 25 | **Breaking changes** |
| 26 | |
| 27 | - **`alchemy deploy` defaults to `live_$USER`** |
| 28 | ([#1421](https://github.com/alchemy-run/alchemy/pull/1421)) — |
| 29 | `plan`, `destroy`, `logs`, and `drift` follow; `alchemy dev` |
| 30 | keeps `dev_$USER`. Pass `--stage dev_sam` to keep deploying to |
| 31 | an old target. The stage env var is now `ALCHEMY_STAGE`, not |
| 32 | `STAGE` ([#1433](https://github.com/alchemy-run/alchemy/pull/1433)). |
| 33 | - **Commands moved** |
| 34 | ([#1234](https://github.com/alchemy-run/alchemy/pull/1234), |
| 35 | [#1235](https://github.com/alchemy-run/alchemy/pull/1235)). The |
| 36 | old names print a migration hint instead of running: |
| 37 | |
| 38 | | Old | New | |
| 39 | | ---------------------------- | ------------------------------------- | |
| 40 | | `alchemy login` | `alchemy profile` | |
| 41 | | `alchemy login --configure` | `alchemy profile edit` | |
| 42 | | `alchemy profile clear` | `alchemy profile delete` | |
| 43 | | `alchemy tail` | `alchemy logs --follow` | |
| 44 | | `alchemy sync` | `alchemy drift --repair` | |
| 45 | | `alchemy destroy --dry-run` | `alchemy plan --destroy` | |
| 46 | | `alchemy aws` / `cloudflare` | `alchemy provider aws` / `cloudflare` | |
| 47 | |
| 48 | - **The AWS `local` auth method is removed** |
| 49 | ([#1480](https://github.com/alchemy-run/alchemy/pull/1480)) — |
| 50 | `alchemy dev` wires the local emulator itself. |
| 51 | - **distilled operations are `verbNoun`** |
| 52 | ([#1517](https://github.com/alchemy-run/alchemy/pull/1517)) — |
| 53 | `railway.serviceCreate` is now `railway.createService`. Only |
| 54 | affects code that calls a distilled client directly. |
| 55 | ::: |
| 56 | |
| 57 | ## 1. Connect an account |
| 58 | |
| 59 | `alchemy login` is now **`alchemy profile`** |
| 60 | ([#1234](https://github.com/alchemy-run/alchemy/pull/1234), |
| 61 | [#1235](https://github.com/alchemy-run/alchemy/pull/1235), |
| 62 | [#1547](https://github.com/alchemy-run/alchemy/pull/1547)). On a |
| 63 | fresh machine it opens on an empty `default` profile: |
| 64 | |
| 65 |  |
| 66 | |
| 67 | `e` lists every provider. Space queues a change on the focused |
| 68 | row — here Cloudflare goes to `add` — and Enter runs the queue: |
| 69 | |
| 70 |  |
| 71 | |
| 72 | Each provider asks only what it needs. Cloudflare offers OAuth or |
| 73 | a token: |
| 74 | |
| 75 |  |
| 76 | |
| 77 | OAuth opens the browser; you approve the grant there: |
| 78 | |
| 79 |  |
| 80 | |
| 81 | Back on the dashboard the provider is a row with its live details: |
| 82 | |
| 83 |  |
| 84 | |
| 85 | Profiles are tabs, `←/→` switches between them, and every profile |
| 86 | holds one connection per provider — a `work` profile with a |
| 87 | Cloudflare token next to a `default` with OAuth + AWS SSO: |
| 88 | |
| 89 |  |
| 90 | |
| 91 | Every key on the dashboard is also a subcommand, and the |
| 92 | subcommands never prompt: |
| 93 | |
| 94 | ```sh |
| 95 | alchemy profile create work |
| 96 | alchemy profile edit --profile work --add cloudflare # runs the OAuth flow |
| 97 | alchemy profile refresh --provider cloudflare |
| 98 | alchemy profile rename work staging |
| 99 | alchemy profile delete staging --yes |
| 100 | ``` |
| 101 | |
| 102 | For agents and CI, token-shaped methods are configured entirely |
| 103 | from flags. `--set` takes a literal, an environment variable, or |
| 104 | stdin, so secrets never touch shell history: |
| 105 | |
| 106 | ```sh |
| 107 | alchemy profile edit --add cloudflare --method stored \ |
| 108 | --set apiToken=env:CLOUDFLARE_API_TOKEN \ |
| 109 | --set accountId=0123456789abcdef0123456789abcdef |
| 110 | |
| 111 | op read op://vault/neon/key | alchemy profile edit --add neon --method stored --set apiKey=- |
| 112 | ``` |
| 113 | |
| 114 | Under the hood, the central `profiles.json` and the separate |
| 115 | `credentials/` tree are gone. A profile is a directory and each |
| 116 | provider is one file holding its method, config, and credentials |
| 117 | together. Existing installs migrate on first run, with the old |
| 118 | layout backed up to `~/.alchemy/.profiles-v0-<timestamp>/` |
| 119 | ([#1429](https://github.com/alchemy-run/alchemy/pull/1429)): |
| 120 | |
| 121 | ```text |
| 122 | ~/.alchemy/profiles/ |
| 123 | ├─ default/ |
| 124 | │ ├─ cloudflare.json # method: oauth, accountId, tokens, scopes |
| 125 | │ └─ aws.json # method: sso, ssoProfile |
| 126 | └─ work/ |
| 127 | └─ cloudflare.json # method: stored, apiToken, accountId |
| 128 | ``` |
| 129 | |
| 130 | Only `profile` commands log in. `deploy`, `plan`, and `dev` never |
| 131 | open a wizard — a missing credential fails with the exact |
| 132 | `alchemy profile edit --add …` to run. Explicit provider |
| 133 | environment variables (`CLOUDFLARE_API_TOKEN`, `AWS_ACCESS_KEY_ID`, |
| 134 | …) take precedence over the profile, per provider, and the CLI |
| 135 | logs which ones it used |
| 136 | ([#1520](https://github.com/alchemy-run/alchemy/pull/1520)). |
| 137 | |
| 138 | Docs: [profile](/cli/profile) · [Profiles](/environments/profiles) |
| 139 | · [CI](/environments/ci). |
| 140 | |
| 141 | ## 2. `alchemy dev` |
| 142 | |
| 143 | The stack in the video is a Worker bound to a KV namespace and an |
| 144 | R2 bucket: |
| 145 | |
| 146 | ```typescript |
| 147 | export default class Api extends Cloudflare.Worker<Api>()( |
| 148 | "Api", |
| 149 | { main: import.meta.url }, |
| 150 | Effect.gen(function* () { |
| 151 | const visits = yield* Cloudflare.KV.ReadWriteNamespace(Visits); |
| 152 | const uploads = yield* Cloudflare.R2.ReadWriteBucket(Uploads); |
| 153 | return { fetch: /* … */ }; |
| 154 | }).pipe( |
| 155 | Effect.provide([ |
| 156 | Cloudflare.KV.ReadWriteNamespaceBinding, |
| 157 | Cloudflare.R2.ReadWriteBucketBinding, |
| 158 | ]), |
| 159 | ), |
| 160 | ) {} |
| 161 | ``` |
| 162 | |
| 163 | `alchemy dev` emulates all three locally. Logs scroll above; the |
| 164 | widget at the bottom shows the stack's output: |
| 165 | |
| 166 |  |
| 167 | |
| 168 | `←/→` flips the widget to the last applied plan — the same |
| 169 | component `deploy`, `drift`, and `destroy` render |
| 170 | ([#1458](https://github.com/alchemy-run/alchemy/pull/1458), |
| 171 | [#1492](https://github.com/alchemy-run/alchemy/pull/1492)). `↑/↓` |
| 172 | scrolls it, `p` hides it: |
| 173 | |
| 174 |  |
| 175 | |
| 176 | Save a file and only the affected resource restarts. Reloads follow |
| 177 | the import graph from your entrypoint — sibling `infra/` and `src/` |
| 178 | trees included — and only fire when a file's contents actually |
| 179 | change ([#1461](https://github.com/alchemy-run/alchemy/pull/1461), |
| 180 | [#1477](https://github.com/alchemy-run/alchemy/pull/1477)): |
| 181 | |
| 182 |  |
| 183 | |
| 184 | ## 3. `alchemy deploy` |
| 185 | |
| 186 | `deploy` shows the plan first. Bindings appear under the resource |
| 187 | they attach to, and the prompt defaults to go: |
| 188 | |
| 189 |  |
| 190 | |
| 191 | Provider logs stream while it runs; the plan ticks over as each |
| 192 | resource lands and the stack's outputs print at the end: |
| 193 | |
| 194 |  |
| 195 | |
| 196 | ```sh |
| 197 | $ curl -s https://demo-api-live-samgoodwin-sthgco4pjlba35wc.testing-2b2.workers.dev |
| 198 | {"message":"Hello from Alchemy","visits":1} |
| 199 | ``` |
| 200 | |
| 201 | ## 4. `alchemy drift` |
| 202 | |
| 203 | Someone renames the KV namespace in the Cloudflare dashboard. |
| 204 | `alchemy drift` re-reads the cloud and diffs it against what was |
| 205 | deployed — the changed attribute renders inline, and `Repair` |
| 206 | puts it back: |
| 207 | |
| 208 |  |
| 209 | |
| 210 |  |
| 211 | |
| 212 | `alchemy drift --repair` does the same without the prompt. |
| 213 | |
| 214 | ## 5. `alchemy destroy` |
| 215 | |
| 216 | Destructive prompts default to `Cancel`: |
| 217 | |
| 218 |  |
| 219 | |
| 220 |  |
| 221 | |
| 222 | ## Without a TTY |
| 223 | |
| 224 | In CI, a pipe, or a coding agent, the same commands print plain |
| 225 | append-only lines instead of the TUI — or force it with |
| 226 | `ALCHEMY_PLAIN=1` / `--no-input` |
| 227 | ([#1423](https://github.com/alchemy-run/alchemy/pull/1423), |
| 228 | [#1426](https://github.com/alchemy-run/alchemy/pull/1426)). Same |
| 229 | deploy as above, plain: |
| 230 | |
| 231 |  |
| 232 | |
| 233 | Exit codes are `0` done / `1` failed or declined / `130` cancelled, |
| 234 | and every run writes a debug log under `~/.alchemy/logs` — the first |
| 235 | thing to attach to a bug report. |
| 236 | |
| 237 | Docs: [CLI](/cli) · [dev](/cli/dev) · [deploy](/cli/deploy) · |
| 238 | [drift](/cli/drift). |
| 239 | |
| 240 | ## `Cloudflare.Telemetry()` |
| 241 | |
| 242 | Effect spans from a Worker now land in |
| 243 | [Workers Observability](https://developers.cloudflare.com/workers/observability/) |
| 244 | ([#1444](https://github.com/alchemy-run/alchemy/pull/1444)). |
| 245 | Provide the Layer on the Worker and your `Effect.withSpan` frames |
| 246 | show up in the Cloudflare dashboard, nested under the platform's |
| 247 | own spans: |
| 248 | |
| 249 | ```typescript |
| 250 | export default Cloudflare.Worker( |
| 251 | "Api", |
| 252 | { main: import.meta.url }, |
| 253 | Effect.gen(function* () { |
| 254 | return { |
| 255 | fetch: Effect.gen(function* () { |
| 256 | const user = yield* kv.get("user:1").pipe(Effect.withSpan("load.user")); |
| 257 | return yield* respond(user); |
| 258 | }).pipe(Effect.withSpan("handle")), |
| 259 | }; |
| 260 | }).pipe(Effect.provide(Cloudflare.Telemetry())), |
| 261 | ); |
| 262 | ``` |
| 263 | |
| 264 | ```text |
| 265 | http.server GET |
| 266 | └─ handle |
| 267 | ├─ load.user |
| 268 | │ └─ kv_get |
| 269 | └─ respond |
| 270 | ``` |
| 271 | |
| 272 | Cloudflare owns sampling and export, so there is no OTLP endpoint |
| 273 | and nothing to flush. It composes with `Axiom.Telemetry` for the |
| 274 | same waterfall in both. The Worker's compatibility date must be |
| 275 | `2026-07-28` or later. |
| 276 | |
| 277 | Docs: [Native Workers tracing](/cloudflare/observability/workers-tracing). |
| 278 | |
| 279 | ## Workflows: cron schedules |
| 280 | |
| 281 | Cloudflare Workflows take wrangler-compatible `schedules`, so a |
| 282 | cron expression creates instances natively — no Cron Trigger |
| 283 | Worker calling `workflow.create()` |
| 284 | ([#1491](https://github.com/alchemy-run/alchemy/pull/1491)): |
| 285 | |
| 286 | ```typescript |
| 287 | export default class Hourly extends Cloudflare.Workflow<Hourly>()( |
| 288 | "Hourly", |
| 289 | { schedules: ["0 * * * *"] }, |
| 290 | Effect.gen(function* () { |
| 291 | return Effect.fn(function* () { |
| 292 | const event = yield* Cloudflare.Workflows.WorkflowEvent; |
| 293 | return event.schedule?.cron; |
| 294 | }); |
| 295 | }), |
| 296 | ) {} |
| 297 | ``` |
| 298 | |
| 299 | ## Provisioned ElastiCache |
| 300 | |
| 301 | `AWS.ElastiCache` adds `SubnetGroup`, `ReplicationGroup` (Valkey |
| 302 | and Redis OSS) and `CacheCluster` (Memcached) alongside the |
| 303 | serverless cache, with `Connect*` bindings that publish the |
| 304 | endpoint to a Lambda and attach it to the cache's VPC |
| 305 | ([#1422](https://github.com/alchemy-run/alchemy/pull/1422)). |
| 306 | Thanks [Henning Pokriefke](https://github.com/henningpokriefke)! |
| 307 | |
| 308 | ```typescript |
| 309 | const cache = yield* AWS.ElastiCache.ReplicationGroup("Cache", { |
| 310 | engine: "valkey", |
| 311 | nodeType: "cache.t4g.micro", |
| 312 | subnetGroupName: subnetGroup.subnetGroupName, |
| 313 | securityGroupIds: [sg.groupId], |
| 314 | replicasPerNodeGroup: 1, |
| 315 | automaticFailoverEnabled: true, |
| 316 | }); |
| 317 | |
| 318 | // inside the Lambda: endpoint env + VPC attachment |
| 319 | const conn = yield* AWS.ElastiCache.ConnectReplicationGroup(cache, { |
| 320 | subnetIds: vpc.privateSubnetIds, |
| 321 | securityGroupIds: [sg.groupId], |
| 322 | }); |
| 323 | ``` |
| 324 | |
| 325 | ## Railway: private services, pre-deploy, local contexts |
| 326 | |
| 327 | Three additions to `Railway.Service`, all from |
| 328 | [Dallen Pyrah](https://github.com/dallenpyrah) — thanks! |
| 329 | ([#1439](https://github.com/alchemy-run/alchemy/pull/1439), |
| 330 | [#1438](https://github.com/alchemy-run/alchemy/pull/1438), |
| 331 | [#1440](https://github.com/alchemy-run/alchemy/pull/1440)) |
| 332 | |
| 333 | ```typescript |
| 334 | const worker = yield* Railway.Service("Worker", { |
| 335 | project, |
| 336 | context: "./worker", // build from a local directory |
| 337 | preDeploy: { command: "bun migrate" }, // runs between build and start |
| 338 | publicDomain: false, // {name}.railway.internal only |
| 339 | }); |
| 340 | ``` |
| 341 | |
| 342 | ## Also in this release |
| 343 | |
| 344 | - **Fly health checks** |
| 345 | ([#1463](https://github.com/alchemy-run/alchemy/pull/1463)) — |
| 346 | `services[].checks` on `Fly.Machine` and `Fly.Service`. Thanks |
| 347 | [Adam Svystun](https://github.com/adamsvystun)! Fly Machines |
| 348 | requests also stop hitting `/v1/v1/…` |
| 349 | ([#1543](https://github.com/alchemy-run/alchemy/pull/1543)). |
| 350 | - **Durable Objects: `webSocketError` and caller context** |
| 351 | ([#1497](https://github.com/alchemy-run/alchemy/pull/1497), |
| 352 | [#1498](https://github.com/alchemy-run/alchemy/pull/1498)) — |
| 353 | `blockConcurrencyWhile` / `storage.transaction` run with the |
| 354 | calling fiber's context. Thanks [Alex](https://github.com/agcty)! |
| 355 | `state.abort(reason, { retryAlarm: false })` stops an interrupted |
| 356 | alarm from retrying |
| 357 | ([#1374](https://github.com/alchemy-run/alchemy/pull/1374)). |
| 358 | - **`Cloudflare.Worker.ref` typechecks** |
| 359 | ([#1434](https://github.com/alchemy-run/alchemy/pull/1434)). |
| 360 | Thanks [pollux](https://github.com/hello-pollux)! |
| 361 | - **Asset uploads run three buckets at a time** |
| 362 | ([#1445](https://github.com/alchemy-run/alchemy/pull/1445)), |
| 363 | matching wrangler; workerd bumped to `20260901.1` |
| 364 | ([#1442](https://github.com/alchemy-run/alchemy/pull/1442)). |
| 365 | Thanks [Rahul Mishra](https://github.com/BlankParticle)! |
| 366 | - **Workflow steps with `retries` but no `timeout` no longer |
| 367 | crash** |
| 368 | ([#1416](https://github.com/alchemy-run/alchemy/pull/1416)). |
| 369 | Thanks [Lord Coughmann](https://github.com/LordCoughmann)! |
| 370 | - **Ctrl+C twice force-quits a slow shutdown** |
| 371 | ([#1427](https://github.com/alchemy-run/alchemy/pull/1427)). |
| 372 | - **`_headers` / `_redirects` no longer crash local dev** |
| 373 | ([#1418](https://github.com/alchemy-run/alchemy/pull/1418)). |
| 374 | Thanks [Michael](https://github.com/deracs)! |
| 375 | - **Worker Loader `get()` returns a usable stub** |
| 376 | ([#1488](https://github.com/alchemy-run/alchemy/pull/1488)) and |
| 377 | **`RpcWorker` keeps its logical ID** |
| 378 | ([#1487](https://github.com/alchemy-run/alchemy/pull/1487)). |
| 379 | - **`nuke` shows progress again** |
| 380 | ([#1532](https://github.com/alchemy-run/alchemy/pull/1532)), and |
| 381 | **`provider cloudflare bootstrap` honors `--profile`** |
| 382 | ([#1538](https://github.com/alchemy-run/alchemy/pull/1538)). |
| 383 | - **PR preview packages live as long as the PR** |
| 384 | ([#1511](https://github.com/alchemy-run/alchemy/pull/1511)). |
| 385 | Thanks [Michael K](https://github.com/Mkassabov)! |
| 386 | - **Docs rewritten as a narrative** |
| 387 | ([#1470](https://github.com/alchemy-run/alchemy/pull/1470), |
| 388 | [#1472](https://github.com/alchemy-run/alchemy/pull/1472)) — |
| 389 | [Getting started](/getting-started), |
| 390 | [What is Alchemy](/what-is-alchemy), and the Workers hub read |
| 391 | top to bottom. |
| 392 | |
| 393 | ## Where to go next |
| 394 | |
| 395 | - [CLI](/cli) · [profile](/cli/profile) · [dev](/cli/dev) · [drift](/cli/drift) |
| 396 | - [Profiles](/environments/profiles) · [CI](/environments/ci) · [Stages](/environments/stages) |
| 397 | - [Native Workers tracing](/cloudflare/observability/workers-tracing) |
| 398 | - [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta77) |
| 399 | - [Compare v2.0.0-beta.76 → v2.0.0-beta.77](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.76...v2.0.0-beta.77) |