1---
2title: "Memoization: builds & one-off commands"
3description: Content-hash memoization shared by Command.Build and Command.Exec — what gets hashed, when a command re-runs, and the outdir contract on destroy.
4---
5
6`Command.Build` and `Command.Exec` content-hash their inputs so a
7deploy skips any build or one-off command whose inputs haven't
8changed — an unchanged frontend isn't rebuilt, unchanged schema
9files don't re-run codegen. Both resources share the same memo
10semantics, covered here. For what the Command provider is and how
11it's registered, start at the [Command hub](/command).
12
13## A memoized build
14
15`Command.Build` runs a command that must produce an output path and
16tracks it in state:
17
18```typescript
19import * as Command from "alchemy/Command";
20import * as Console from "effect/Console";
21
22const build = yield* Command.Build("vite-build", {
23 command: "npm run build",
24 cwd: "./frontend",
25 outdir: "dist",
26});
27yield* Console.log(build.outdir); // path to the dist directory, relative to process.cwd()
28yield* Console.log(build.hash.output); // hash of the output files (when memo is enabled)
29```
30
31Reconcile runs the command, then verifies it actually produced
32`outdir` — if the path doesn't exist afterwards, the deploy fails
33with a `CommandError` whose reason is `OutputNotFound`. On success
34the resource records two hashes: `hash.input` (the files that
35produced the build) and `hash.output` (the files inside `outdir`).
36
37## What gets hashed
38
39By default, every non-gitignored file under `cwd`: `include`
40defaults to `["**/*"]`, and `exclude` defaults to the `.gitignore`
41rules collected from `cwd` up to the repository root, plus
42`**/.git/**`. The nearest package-manager lockfile (`bun.lock`,
43`package-lock.json`, `pnpm-lock.yaml`, or `yarn.lock`, found by
44walking upward — so a monorepo-root lockfile counts) is hashed too.
45Narrow the scope with explicit globs when the default is too broad:
46
47```typescript
48yield* Command.Exec("codegen", {
49 command: "npm run codegen",
50 memo: { include: ["schema/**"] },
51});
52```
53
54The lockfile default only applies when both `include` and `exclude`
55are unset — with custom globs like the above, set `lockfile: true`
56explicitly if you still want it in the hash. Setting `memo: false`
57disables hashing entirely and re-runs the command on every deploy.
58
59## When it re-runs
60
61Diff rehashes the inputs on every deploy and returns `noop` when
62nothing changed, so the command never runs. A change to any prop —
63`command`, `cwd`, `env` — forces a re-run even though the file
64hashes are unchanged (e.g. a recreated database's connection URL
65with identical files: the hash tracks input files only, so the
66re-run is driven by the prop change, not the hash). For `Build`,
67diff rehashes the output too, so a deleted `outdir` triggers a
68rebuild even with unchanged inputs — `rm -rf dist` followed by a
69deploy rebuilds instead of silently no-oping.
70
71## The outdir contract
72
73`build.outdir` is stored relative to `process.cwd()`, not absolute,
74so state is portable across machines — a CI runner's
75`/home/runner/work/.../dist` resolves correctly on a local laptop
76and vice versa. Consumers resolve it against their own cwd:
77
78```typescript
79import * as Path from "effect/Path";
80
81const path = yield* Path.Path;
82const absolute = path.resolve(build.outdir);
83```
84
85On destroy, `Build`'s delete removes the outdir recursively —
86tearing down the stack cleans up the build artifacts along with the
87state.
88
89## One-off commands
90
91`Command.Exec` runs a command purely for its side effects — codegen,
92seeding data, setup steps — and succeeds as long as the exit code is
93`0`. It memoizes the same way as `Build`, minus the output half:
94only `hash.input` is recorded.
95
96```typescript
97import * as Redacted from "effect/Redacted";
98
99yield* Command.Exec("migrate", {
100 command: "npm run db:migrate",
101 env: {
102 DATABASE_URL: Redacted.make("postgres://..."),
103 },
104});
105```
106
107`Redacted` env values are unwrapped at spawn time, so the secret
108reaches the child process without appearing in logs or state. The
109full re-run matrix:
110
111- Unchanged inputs — the run is skipped.
112- A changed memoized file, an env-only change, or a command-only
113 change — each re-runs the command.
114- Destroy — delete is a no-op; the command's side effects are never
115 reversed.
116- After a destroy, an unchanged redeploy runs the command again —
117 destroy forgets the run key.
118
119## Where next
120
121- [Static site](/cloudflare/frontend/static-site) — deploy the build
122 output; `StaticSite` is a `Command.Build` resource under the hood.
123- [Dev servers](/command/dev-servers) — long-lived local processes
124 with `Command.Dev` during `alchemy dev`.
125
126Reference:
127
128- [Build](/providers/command/build) ·
129 [Exec](/providers/command/exec)