Skip to content

2.0.0-beta.77 - Profiles, CLI & Workers Tracing

beta.77 redesigns the CLI. alchemy login becomes alchemy profile, a dashboard for every connected account whose every action is also a non-interactive subcommand; profiles are stored as a folder per profile with a file per provider instead of a central profiles.json; and alchemy dev gets a plan/output widget you can flip with ←/→. Outside the CLI, Cloudflare.Telemetry() sends Effect spans to Workers Observability with no OTLP setup, Workflows take cron schedules, and AWS gains provisioned ElastiCache.

Here is the new CLI on a fresh machine, start to finish:

alchemy profilealchemy devalchemy deployalchemy driftalchemy destroy. The same walkthrough, frame by frame, is below.

alchemy login is now alchemy profile (#1234, #1235, #1547). On a fresh machine it opens on an empty default profile:

alchemy profile on a fresh machine: one empty default profile

e lists every provider. Space queues a change on the focused row — here Cloudflare goes to add — and Enter runs the queue:

the edit menu with Cloudflare queued to add

Each provider asks only what it needs. Cloudflare offers OAuth or a token:

choosing OAuth as the Cloudflare authentication method

OAuth opens the browser; you approve the grant there:

Cloudflare’s consent page for Alchemy

Back on the dashboard the provider is a row with its live details:

the dashboard after connecting: Cloudflare via oauth, token expiry, account id

Profiles are tabs, ←/→ switches between them, and every profile holds one connection per provider — a work profile with a Cloudflare token next to a default with OAuth + AWS SSO:

two profiles, each with several providers

Every key on the dashboard is also a subcommand, and the subcommands never prompt:

Terminal window
alchemy profile create work
alchemy profile edit --profile work --add cloudflare # runs the OAuth flow
alchemy profile refresh --provider cloudflare
alchemy profile rename work staging
alchemy profile delete staging --yes

For agents and CI, token-shaped methods are configured entirely from flags. --set takes a literal, an environment variable, or stdin, so secrets never touch shell history:

Terminal window
alchemy profile edit --add cloudflare --method stored \
--set apiToken=env:CLOUDFLARE_API_TOKEN \
--set accountId=0123456789abcdef0123456789abcdef
op read op://vault/neon/key | alchemy profile edit --add neon --method stored --set apiKey=-

Under the hood, the central profiles.json and the separate credentials/ tree are gone. A profile is a directory and each provider is one file holding its method, config, and credentials together. Existing installs migrate on first run, with the old layout backed up to ~/.alchemy/.profiles-v0-<timestamp>/ (#1429):

~/.alchemy/profiles/
├─ default/
│ ├─ cloudflare.json # method: oauth, accountId, tokens, scopes
│ └─ aws.json # method: sso, ssoProfile
└─ work/
└─ cloudflare.json # method: stored, apiToken, accountId

Only profile commands log in. deploy, plan, and dev never open a wizard — a missing credential fails with the exact alchemy profile edit --add … to run. Explicit provider environment variables (CLOUDFLARE_API_TOKEN, AWS_ACCESS_KEY_ID, …) take precedence over the profile, per provider, and the CLI logs which ones it used (#1520).

Docs: profile · Profiles · CI.

The stack in the video is a Worker bound to a KV namespace and an R2 bucket:

export default class Api extends Cloudflare.Worker<Api>()(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
const visits = yield* Cloudflare.KV.ReadWriteNamespace(Visits);
const uploads = yield* Cloudflare.R2.ReadWriteBucket(Uploads);
return { fetch: /* … */ };
}).pipe(
Effect.provide([
Cloudflare.KV.ReadWriteNamespaceBinding,
Cloudflare.R2.ReadWriteBucketBinding,
]),
),
) {}

alchemy dev emulates all three locally. Logs scroll above; the widget at the bottom shows the stack’s output:

alchemy dev running, output widget showing the local url and bucket

←/→ flips the widget to the last applied plan — the same component deploy, drift, and destroy render (#1458, #1492). ↑/↓ scrolls it, p hides it:

the same widget flipped to the plan: 3 created

Save a file and only the affected resource restarts. Reloads follow the import graph from your entrypoint — sibling infra/ and src/ trees included — and only fire when a file’s contents actually change (#1461, #1477):

after editing the worker: 1 updated, 2 no change

deploy shows the plan first. Bindings appear under the resource they attach to, and the prompt defaults to go:

the deploy plan: 3 to create, 2 binding changes, Deploy highlighted

Provider logs stream while it runs; the plan ticks over as each resource lands and the stack’s outputs print at the end:

stack deployed: url and bucket outputs

Terminal window
$ curl -s https://demo-api-live-samgoodwin-sthgco4pjlba35wc.testing-2b2.workers.dev
{"message":"Hello from Alchemy","visits":1}

Someone renames the KV namespace in the Cloudflare dashboard. alchemy drift re-reads the cloud and diffs it against what was deployed — the changed attribute renders inline, and Repair puts it back:

drift detected: title changed on the KV namespace, Repair highlighted

the repair applied: 1 updated

alchemy drift --repair does the same without the prompt.

Destructive prompts default to Cancel:

the destroy plan: 3 to delete, Cancel highlighted

stack destroyed

In CI, a pipe, or a coding agent, the same commands print plain append-only lines instead of the TUI — or force it with ALCHEMY_PLAIN=1 / --no-input (#1423, #1426). Same deploy as above, plain:

ALCHEMY_PLAIN=1 alchemy deploy –yes: timestamped log lines for the plan, each create, Done: 3 succeeded, and the outputs

Exit codes are 0 done / 1 failed or declined / 130 cancelled, and every run writes a debug log under ~/.alchemy/logs — the first thing to attach to a bug report.

Docs: CLI · dev · deploy · drift.

Effect spans from a Worker now land in Workers Observability (#1444). Provide the Layer on the Worker and your Effect.withSpan frames show up in the Cloudflare dashboard, nested under the platform’s own spans:

export default Cloudflare.Worker(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
return {
fetch: Effect.gen(function* () {
const user = yield* kv.get("user:1").pipe(Effect.withSpan("load.user"));
return yield* respond(user);
}).pipe(Effect.withSpan("handle")),
};
}).pipe(Effect.provide(Cloudflare.Telemetry())),
);
http.server GET
└─ handle
├─ load.user
│ └─ kv_get
└─ respond

Cloudflare owns sampling and export, so there is no OTLP endpoint and nothing to flush. It composes with Axiom.Telemetry for the same waterfall in both. The Worker’s compatibility date must be 2026-07-28 or later.

Docs: Native Workers tracing.

Cloudflare Workflows take wrangler-compatible schedules, so a cron expression creates instances natively — no Cron Trigger Worker calling workflow.create() (#1491):

export default class Hourly extends Cloudflare.Workflow<Hourly>()(
"Hourly",
{ schedules: ["0 * * * *"] },
Effect.gen(function* () {
return Effect.fn(function* () {
const event = yield* Cloudflare.Workflows.WorkflowEvent;
return event.schedule?.cron;
});
}),
) {}

AWS.ElastiCache adds SubnetGroup, ReplicationGroup (Valkey and Redis OSS) and CacheCluster (Memcached) alongside the serverless cache, with Connect* bindings that publish the endpoint to a Lambda and attach it to the cache’s VPC (#1422). Thanks Henning Pokriefke!

const cache = yield* AWS.ElastiCache.ReplicationGroup("Cache", {
engine: "valkey",
nodeType: "cache.t4g.micro",
subnetGroupName: subnetGroup.subnetGroupName,
securityGroupIds: [sg.groupId],
replicasPerNodeGroup: 1,
automaticFailoverEnabled: true,
});
// inside the Lambda: endpoint env + VPC attachment
const conn = yield* AWS.ElastiCache.ConnectReplicationGroup(cache, {
subnetIds: vpc.privateSubnetIds,
securityGroupIds: [sg.groupId],
});

Railway: private services, pre-deploy, local contexts

Section titled “Railway: private services, pre-deploy, local contexts”

Three additions to Railway.Service, all from Dallen Pyrah — thanks! (#1439, #1438, #1440)

const worker = yield* Railway.Service("Worker", {
project,
context: "./worker", // build from a local directory
preDeploy: { command: "bun migrate" }, // runs between build and start
publicDomain: false, // {name}.railway.internal only
});
  • Fly health checks (#1463) — services[].checks on Fly.Machine and Fly.Service. Thanks Adam Svystun! Fly Machines requests also stop hitting /v1/v1/… (#1543).
  • Durable Objects: webSocketError and caller context (#1497, #1498) — blockConcurrencyWhile / storage.transaction run with the calling fiber’s context. Thanks Alex! state.abort(reason, { retryAlarm: false }) stops an interrupted alarm from retrying (#1374).
  • Cloudflare.Worker.ref typechecks (#1434). Thanks pollux!
  • Asset uploads run three buckets at a time (#1445), matching wrangler; workerd bumped to 20260901.1 (#1442). Thanks Rahul Mishra!
  • Workflow steps with retries but no timeout no longer crash (#1416). Thanks Lord Coughmann!
  • Ctrl+C twice force-quits a slow shutdown (#1427).
  • _headers / _redirects no longer crash local dev (#1418). Thanks Michael!
  • Worker Loader get() returns a usable stub (#1488) and RpcWorker keeps its logical ID (#1487).
  • nuke shows progress again (#1532), and provider cloudflare bootstrap honors --profile (#1538).
  • PR preview packages live as long as the PR (#1511). Thanks Michael K!
  • Docs rewritten as a narrative (#1470, #1472) — Getting started, What is Alchemy, and the Workers hub read top to bottom.