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.
Declaring an Action
Section titled “Declaring an Action”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.
Multiple instances
Section titled “Multiple instances”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 });Tagged form (service + layer)
Section titled “Tagged form (service + layer)”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.
Binding resources
Section titled “Binding resources”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.
Reading a resource’s Outputs
Section titled “Reading a resource’s Outputs”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.
Lifecycle
Section titled “Lifecycle”An Action has only two terminal states:
| Action | Symbol | When |
|---|---|---|
| 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.
Input hashing
Section titled “Input hashing”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”.
Forcing a re-run
Section titled “Forcing a re-run”alchemy deploy --force--force flips every skip to run at plan time, including actions.
Dependencies
Section titled “Dependencies”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.
What Actions are not
Section titled “What Actions are not”- 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
runningstate is persisted but not its side effects).