1---
2title: Actions
3description: A node in the dependency graph that runs an Effect during apply when its inputs change.
4---
5
6An **Action** is a node in the stack's dependency graph that runs an
7arbitrary Effect during `apply`. Unlike a [Resource](/infrastructure-as-code/resource), it has
8no provider lifecycle — no replace, no read, no delete. The engine just
9diffs the resolved input against the last persisted hash and either runs
10the body or skips it. If Outputs or the plan/persisted-state model are
11new to you, [Outputs](/infrastructure-as-code/outputs) and
12[Resource Lifecycle](/infrastructure-as-code/resource-lifecycle) are the
13background this page builds on.
14
15Actions are useful for one-off deploy-time work that needs to be
16reproducible and dependency-aware: seeding a database, posting a
17release notification, generating an artifact and uploading it,
18invalidating a CDN cache, running a migration check.
19
20## Declaring an Action
21
22Define an Action with its type name and a body, then call it inside a
23stack to register an instance. `yield*` returns `Output<Out>` ready to
24feed into downstream nodes:
25
26```typescript
27const Sync = Action("Sync", Effect.fn(function* (input: { table: string }) {
28 yield* Effect.log(`syncing ${input.table}`);
29 return { rows: 42 };
30}));
31
32// In a stack — default form uses the Type as the LogicalId:
33const rows = yield* Sync({ table: bucket.name });
34// ^ Output<{ rows: number }>
35rows.rows // Output<number>
36```
37
38The body Effect receives the **resolved** input — any
39[Output](/infrastructure-as-code/outputs) references in the input are evaluated against the
40current tracker before the body runs.
41
42### Constructor (pulling in dependencies)
43
44Pass an Effect that yields the runner instead of the runner itself.
45The constructor Effect can `yield*` services, and those dependencies surface
46as `Req` on the call site:
47
48```typescript
49const Sync = Action("Sync", Effect.gen(function* () {
50 const db = yield* Database;
51 const logger = yield* Logger;
52 return Effect.fn(function* (input: { table: string }) {
53 yield* logger.info(`syncing ${input.table}`);
54 return { rows: yield* db.count(input.table) };
55 });
56}));
57
58// `yield* Sync({...})` now requires `Database | Logger | Stack`.
59```
60
61The constructor runs at most once per process and the resolved runner is
62reused across every instance and re-run.
63
64### Multiple instances
65
66Pass an explicit logical id to register more than one instance of the
67same Action definition:
68
69```typescript
70const nightly = yield* Sync("nightly", { table: usersBucket.name });
71const hourly = yield* Sync("hourly", { table: eventsBucket.name });
72```
73
74### Tagged form (service + layer)
75
76When you want to split the contract from the implementation — e.g. for
77testing or to keep stack code declarative — declare the type with an
78`interface`, build the value with the no-argument overload, then supply
79the runner separately with `.make`:
80
81```typescript
82export interface Sync extends Action<"Sync", { table: string }, { rows: number }> {}
83export const Sync = Action<Sync, { table: string }, { rows: number }>()("Sync");
84
85export const SyncLive = Sync.make(
86 Effect.gen(function* () {
87 const db = yield* Database;
88 return Effect.fn(function* (input) {
89 return { rows: yield* db.count(input.table) };
90 });
91 }),
92);
93
94// In a stack:
95const rows = yield* Sync({ table: bucket.name });
96// ^ requires `Sync` — add `SyncLive` to the stack's providers,
97// or provide it locally with `Effect.provide(SyncLive)`.
98```
99
100`.make(...)` accepts either a direct runner or a constructor Effect, and the
101constructor runs under the same context as the inline form — so the resource
102bindings and Output accessors below work here too.
103
104## Binding resources
105
106An Action's body often needs to *talk to* the resources in your stack —
107seed a database, warm a cache, enqueue a job.
108[Bindings](/infrastructure-as-effects/binding) like
109[`Cloudflare.D1.QueryDatabase`](/cloudflare/data/d1) normally resolve
110against a deployed Worker's runtime environment, which an Action doesn't
111have. Provide the binding's **`*Local`** layer instead: it talks to the
112service over the provider's HTTP API using your current CLI credentials.
113
114```typescript
115const Seed = Action(
116 "Seed",
117 Effect.gen(function* () {
118 const db = yield* Cloudflare.D1.QueryDatabase(database);
119 return Effect.fn(function* () {
120 yield* db.exec("CREATE TABLE IF NOT EXISTS users (id TEXT, name TEXT)");
121 yield* db
122 .prepare("INSERT INTO users (id, name) VALUES (?, ?)")
123 .bind("1", "Ada")
124 .run();
125 });
126 }).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseLocal)),
127);
128```
129
130`*Local` is a third binding variant alongside the native Worker binding
131(`*Binding`) and the scoped-token HTTP client (`*Http`). It registers no
132binding on a host and mints no token — it reuses the credentials Alchemy
133is already deploying with. The runtime client is identical, so the same
134`db.prepare(...).run()` code works whether it runs inside a deployed
135Worker or a deploy-time Action.
136
137Local layers exist for every Cloudflare capability with an HTTP data
138plane — D1, KV, R2, Queues, DNS, Vectorize, Tunnel, AI Search, Flagship,
139and Browser Rendering. Worker-runtime-only bindings (Rate Limiting,
140Version Metadata, service bindings, …) have no Local variant.
141
142### Reading a resource's Outputs
143
144Inside an Action you can `yield*` a resource
145[Output](/infrastructure-as-code/outputs) to get an accessor that
146resolves at apply time — after the resource exists:
147
148```typescript
149const Seed = Action(
150 "Seed",
151 Effect.gen(function* () {
152 const databaseId = yield* database.databaseId;
153 // ^ deferred accessor — not the value yet
154 return Effect.fn(function* () {
155 const id = yield* databaseId; // resolved during apply
156 yield* Effect.log(`seeding ${id}`);
157 });
158 }),
159);
160```
161
162Capturing an Output this way also makes the Action **depend** on that
163resource, so it runs after the resource is created — the same edge you'd
164get from passing `database.databaseId` as input. This works in both the
165inline and tagged `.make` forms.
166
167## Lifecycle
168
169An Action has only two terminal states:
170
171| Action | Symbol | When |
172| --- | --- | --- |
173| **run** | `λ` | First time, or `inputHash` differs from the last persisted run, or `--force` is set |
174| **skip** | `·` | Persisted `inputHash` matches the newly resolved input |
175
176There is no `replace` and no `delete`. When an Action is removed from the
177stack, its persisted state is dropped without the body being invoked.
178
179### Input hashing
180
181The Action's input is JSON-serialized and SHA-256 hashed after upstream
182Outputs are resolved. The hash is persisted alongside the result; on
183the next plan, a new hash that matches means "skip", a new hash that
184differs means "run".
185
186### Forcing a re-run
187
188```sh
189alchemy deploy --force
190```
191
192`--force` flips every `skip` to `run` at plan time, including actions.
193
194### Dependencies
195
196Actions live in the same FQN namespace as Resources. They can:
197
198- Take Resource outputs as input (`{ table: bucket.name }`)
199- Capture a Resource Output in the constructor (`yield* bucket.name`) — see
200 [Reading a resource's Outputs](#reading-a-resources-outputs)
201- Be referenced by Resources via `action.output` (downstream resource
202 waits for the action before reconciling)
203- Reference other Actions
204
205Cycles are rejected at plan time just like resource cycles.
206
207## What Actions are not
208
209- **Not a Resource.** No `diff`/`read`/`reconcile`/`delete`. If you
210 need lifecycle management of a cloud entity, model it as a Resource.
211- **Not a runtime function.** An Action runs at deploy time. To call code
212 from a deployed Worker or Lambda, see [Runtime](/infrastructure-as-effects/runtime).
213- **Not idempotent for free.** The engine guarantees the body runs
214 only when inputs change, but the body itself must tolerate retries
215 on apply restart (its `running` state is persisted but not its
216 side effects).
217
218## Where next
219
220- [Inputs & Outputs](/infrastructure-as-code/outputs) — the lazy values
221 Actions take as input and produce. Next page.
222- [Resource Lifecycle](/infrastructure-as-code/resource-lifecycle) — the
223 plan/apply model Actions participate in.
224- [Bindings](/infrastructure-as-effects/binding) — the runtime clients
225 Actions reuse via `*Local` layers.