Skip to content

Actions

An Action is a node in the stack’s dependency graph that runs an arbitrary Effect during apply. Unlike a Resource, it has no provider lifecycle — no replace, no read, no delete. The engine just diffs the resolved input against the last persisted hash and either runs the body or skips it.

Actions are useful for one-off deploy-time work that needs to be reproducible and dependency-aware: seeding a database, posting a release notification, generating an artifact and uploading it, invalidating a CDN cache, running a migration check.

Define an Action with its type name and a body, then call it inside a stack to register an instance. yield* returns Output<Out> ready to feed into downstream nodes:

const Sync = Action("Sync", Effect.fn(function* (input: { table: string }) {
yield* Effect.log(`syncing ${input.table}`);
return { rows: 42 };
}));
// In a stack — default form uses the Type as the LogicalId:
const rows = yield* Sync({ table: bucket.name });
// ^ Output<{ rows: number }>
rows.rows // Output<number>

The body Effect receives the resolved input — any Output references in the input are evaluated against the current tracker before the body runs.

Init constructor (pulling in dependencies)

Section titled “Init constructor (pulling in dependencies)”

Pass an Effect that yields the runner instead of the runner itself. The init Effect can yield* services, and those dependencies surface as Req on the call site:

const Sync = Action("Sync", Effect.gen(function* () {
const db = yield* Database;
const logger = yield* Logger;
return Effect.fn(function* (input: { table: string }) {
yield* logger.info(`syncing ${input.table}`);
return { rows: yield* db.count(input.table) };
});
}));
// `yield* Sync({...})` now requires `Database | Logger | Stack`.

The init runs at most once per process and the resolved runner is reused across every instance and re-run.

Pass an explicit logical id to register more than one instance of the same Action definition:

const nightly = yield* Sync("nightly", { table: usersBucket.name });
const hourly = yield* Sync("hourly", { table: eventsBucket.name });

When you want to split the contract from the implementation — e.g. for testing or to keep stack code declarative — declare the type with an interface, build the value with the no-argument overload, then supply the runner separately with .make:

export interface Sync extends Action<"Sync", { table: string }, { rows: number }> {}
export const Sync = Action<Sync, { table: string }, { rows: number }>()("Sync");
export const SyncLive = Sync.make(
Effect.gen(function* () {
const db = yield* Database;
return Effect.fn(function* (input) {
return { rows: yield* db.count(input.table) };
});
}),
);
// In a stack:
const rows = yield* Sync({ table: bucket.name });
// ^ requires `Sync` — add `SyncLive` to the stack's providers,
// or provide it locally with `Effect.provide(SyncLive)`.

.make(...) accepts either a direct runner or an init Effect, and the init runs under the same context as the inline form — so the resource bindings and Output accessors below work here too.

An Action’s body often needs to talk to the resources in your stack — seed a database, warm a cache, enqueue a job. Bindings like Cloudflare.D1.QueryDatabase normally resolve against a deployed Worker’s runtime environment, which an Action doesn’t have. Provide the binding’s *Local layer instead: it talks to the service over the provider’s HTTP API using your current CLI credentials.

const Seed = Action(
"Seed",
Effect.gen(function* () {
const db = yield* Cloudflare.D1.QueryDatabase(database);
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();
});
}).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseLocal)),
);

*Local is a third binding variant alongside the native Worker binding (*Binding) and the scoped-token HTTP client (*Http). It registers no binding on a host and mints no token — it reuses the credentials Alchemy is already deploying with. The runtime client is identical, so the same db.prepare(...).run() code works whether it runs inside a deployed Worker or a deploy-time Action.

Local layers exist for every Cloudflare capability with an HTTP data plane — D1, KV, R2, Queues, DNS, Vectorize, Tunnel, AI Search, Flagship, and Browser Rendering. Worker-runtime-only bindings (Rate Limiting, Version Metadata, service bindings, …) have no Local variant.

Inside an Action you can yield* a resource Output to get an accessor that resolves at apply time — after the resource exists:

const Seed = Action(
"Seed",
Effect.gen(function* () {
const databaseId = yield* database.databaseId;
// ^ deferred accessor — not the value yet
return Effect.fn(function* () {
const id = yield* databaseId; // resolved during apply
yield* Effect.log(`seeding ${id}`);
});
}),
);

Capturing an Output this way also makes the Action depend on that resource, so it runs after the resource is created — the same edge you’d get from passing database.databaseId as input. This works in both the inline and tagged .make forms.

An Action has only two terminal states:

ActionSymbolWhen
runλFirst time, or inputHash differs from the last persisted run, or --force is set
skip·Persisted inputHash matches the newly resolved input

There is no replace and no delete. When an Action is removed from the stack, its persisted state is dropped without the body being invoked.

The Action’s input is JSON-serialized and SHA-256 hashed after upstream Outputs are resolved. The hash is persisted alongside the result; on the next plan, a new hash that matches means “skip”, a new hash that differs means “run”.

Terminal window
alchemy deploy --force

--force flips every skip to run at plan time, including actions.

Actions live in the same FQN namespace as Resources. They can:

  • Take Resource outputs as input ({ table: bucket.name })
  • Capture a Resource Output in the init (yield* bucket.name) — see Reading a resource’s Outputs
  • Be referenced by Resources via action.output (downstream resource waits for the action before reconciling)
  • Reference other Actions

Cycles are rejected at plan time just like resource cycles.

  • Not a Resource. No diff/read/reconcile/delete. If you need lifecycle management of a cloud entity, model it as a Resource.
  • Not a runtime function. An Action runs at deploy time. To call code from a deployed Worker or Lambda, see Functions & Servers.
  • Not idempotent for free. The engine guarantees the body runs only when inputs change, but the body itself must tolerate retries on apply restart (its running state is persisted but not its side effects).