Skip to content

feat(action): local bindings + Output accessors in Actions#807

Open
sam-goodwin wants to merge 5 commits into
mainfrom
claude/alchemy-actions-local-bindings-825ac1
Open

feat(action): local bindings + Output accessors in Actions#807
sam-goodwin wants to merge 5 commits into
mainfrom
claude/alchemy-actions-local-bindings-825ac1

Conversation

@sam-goodwin

@sam-goodwin sam-goodwin commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Actions can now bind resources with the current credentials and read resource Outputs, the same way a Cloudflare.Worker does.

const Seed = Alchemy.Action(
  "Seed",
  Effect.gen(function* () {
    const db = yield* Cloudflare.D1.QueryDatabase(database);
    const databaseId = yield* database.databaseId; // accessor captured at init

    return Effect.fn(function* () {
      yield* db.exec("CREATE TABLE IF NOT EXISTS users (id TEXT, name TEXT)");
      yield* db
        .prepare("INSERT INTO users (id, name) VALUES (?, ?)")
        .bind("1", "Ada")
        .run();
      return { databaseId: yield* databaseId };
    });
  }).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseLocal)),
);

*Local binding layers

A third binding variant alongside *Binding (native Worker) and *Http (scoped token). A *Local layer resolves the current CLI credentials (ambient during stack-eval via the stack's providers context), never touches a Worker host or host.bind, and returns the same client backed by the provider's HTTP API. Where an *Http variant already exists, its client builder is refactored to take an injectable { authorize, accountId } auth shared by both the token-scoped Http and the current-creds Local; otherwise the Local layer implements a direct HTTP client (D1's shim is the reference).

Implemented for every Cloudflare capability with an HTTP data-plane — each live-tested against real Cloudflare:

Service Local layers
D1 QueryDatabaseLocal
KV ReadNamespaceLocal / WriteNamespaceLocal / ReadWriteNamespaceLocal
R2 ReadBucketLocal / WriteBucketLocal / ReadWriteBucketLocal
Queues WriteQueueLocal
DNS ReadDnsLocal / WriteDnsLocal / ReadWriteDnsLocal
Vectorize SearchIndexLocal
Tunnel ReadTunnelLocal / WriteTunnelLocal / ReadWriteTunnelLocal
AI Search QuerySearchLocal / QuerySearchNamespaceLocal
Flagship ReadFlagsLocal
Workers BrowserLocal (Browser Rendering REST API)

Worker-runtime-only bindings have no Local variant and are intentionally excluded (no account HTTP data-plane): SecretsStore (write-only secret values), AnalyticsEngine (write-only ingestion), Images (in-Worker transforms), Hyperdrive (connection pooler), Email (send_email), Addressing, Artifacts, WorkersForPlatforms dispatch, AI QueryGateway (inference), RateLimit, VersionMetadata, and service bindings.

Output accessors in Actions

An Action's init runs at stack-eval (before resources exist); its body runs at apply. To make yield* db.databaseId work, RuntimeContext is split into two cooperating halves:

  • a capture context provided around the init — records referenced Outputs and hands back deferred accessors
  • a resolve context provided around the body — returns values evaluated against the tracker

Captured Outputs also become dependency edges, so the Action waits for the bound resource. Works in both the inline form and the tagged .make form.

Verification

  • test/action.test.ts — inline + tagged .make capture/accessor tests.
  • One live *Local test per implemented service (deploy resource + Action driving the Local binding).
  • Workspace bun tsc -b clean.

Follow-up

Vectorize insertIndex/upsertIndex are generated with a multipart part named body, but the v2 API requires a part named vectors; SearchIndexLocal works around it in-layer. A distilled generator/spec fix would let every consumer get the correct multipart field for free.

Actions can now bind resources with current credentials and read
resource Outputs, like a Cloudflare Worker.

- ActionRuntimeContext: `yield* db.databaseId` inside an Action init
  captures the Output (→ dependency edge) and resolves it against the
  tracker at apply time. Works in both the inline and tagged `.make`
  forms.
- Cloudflare.D1.QueryDatabaseLocal: a `*Local` layer (third variant
  alongside `*Binding`/`*Http`) that queries D1 over HTTP with the
  current credentials — no Worker host, no `host.bind`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alchemy-version-bot

alchemy-version-bot Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

alchemy

bun add alchemy@https://pkg.ing/alchemy/031d538

@alchemy.run/better-auth

bun add @alchemy.run/better-auth@https://pkg.ing/@alchemy.run/better-auth/031d538

@alchemy.run/pr-package

bun add @alchemy.run/pr-package@https://pkg.ing/@alchemy.run/pr-package/031d538

sam-goodwin and others added 4 commits July 11, 2026 00:22
*Local layers (current-credentials, over HTTP) for the KV, R2, and
Queues bindings so they can be used inside an Action. Each refactors
its shared *Http client builder to accept an injectable auth
({ authorize, accountId }) reused by both the token-scoped Http variant
and the current-creds Local variant.

- KV: ReadNamespaceLocal / WriteNamespaceLocal / ReadWriteNamespaceLocal
- R2: ReadBucketLocal / WriteBucketLocal / ReadWriteBucketLocal
- Queues: WriteQueueLocal

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…h, Flagship, Browser

Extends the *Local (current-credentials, HTTP) binding pattern to every
Cloudflare capability with an HTTP data-plane, so they can be used inside
an Action:

- DNS:       ReadDnsLocal / WriteDnsLocal / ReadWriteDnsLocal
- Vectorize: SearchIndexLocal
- Tunnel:    ReadTunnelLocal / WriteTunnelLocal / ReadWriteTunnelLocal
- AI Search: QuerySearchLocal / QuerySearchNamespaceLocal
- Flagship:  ReadFlagsLocal
- Workers:   BrowserLocal (Browser Rendering REST API)

Each reuses/refactors its *Http client builder via an injectable
{ authorize, accountId } auth (shared by token-scoped Http and
current-creds Local), or a direct HTTP client where no *Http existed.
Every layer is live-tested against real Cloudflare.

Worker-runtime-only bindings have no Local variant and are intentionally
excluded: SecretsStore (write-only values), AnalyticsEngine (write-only
ingestion), Images (in-Worker transforms), Hyperdrive (pooler), Email
(send_email), Addressing, Artifacts, WorkersForPlatforms dispatch, AI
QueryGateway, RateLimit, VersionMetadata, service bindings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mData shim

Now that distilled models the vectorize insert/upsert multipart part as
`vectors`, SearchIndexLocal calls `vectorize.insertIndex`/`upsertIndex`
directly instead of hand-building a FormData request. Bumps the distilled
submodule to the regenerated service.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rm .make

- Add "Binding resources" section: bind a resource inside an Action via
  its *Local layer (current-credentials HTTP), with the D1 seed example.
- Add "Reading a resource's Outputs": `yield* resource.attr` accessors
  resolved at apply time + the dependency edge they create.
- Fix the tagged form example: use the value form (interface + const),
  not the non-constructable `class extends` shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant