Resource Lifecycle
Every Resource goes through the same lifecycle:
plan → reconcile → (replace) → delete. The plan classifies each
resource as create, update, replace, delete, or no-op, but the
provider implements a single reconcile function that converges the
cloud’s actual state to what’s declared — whether that’s the first
provisioning, a routine update, or an adoption takeover. For the CLI
flags that drive these operations, see the
CLI reference.
The lifecycle, end-to-end
Section titled “The lifecycle, end-to-end”Both create and update intents resolve to the provider’s single
reconcile function. Replace runs reconcile against a fresh
instance id and then deletes the old generation. The same engine
drives alchemy deploy, alchemy destroy, and alchemy dev.
When you run alchemy deploy, alchemy first plans the change.
It compares the desired state (what your code declares) against the
last persisted state and classifies each resource:
- Create (
+) — declared in code, not in state - Update (
~) — declared and persisted, but properties differ - Replace (
±) — change requires destroy-and-recreate - Delete (
-) — persisted but no longer declared - No-op (
•) — unchanged
Plan: 1 to create, 1 to update + Queue (AWS.SQS.Queue) ~ Worker (Cloudflare.Worker) • Bucket (Cloudflare.R2.Bucket)
The classification comes from each provider’s diff function. See
Provider › diff for how providers decide
between in-place updates and replacements.
Use alchemy plan (or alchemy deploy --dry-run) to see the plan
without applying it.
Reconcile
Section titled “Reconcile”Whether a resource is being created for the first time, updated in
place, or adopted from existing infrastructure, alchemy calls a
single function: provider.reconcile.
Reconcile must be convergent: given the desired props, it
observes live cloud state and brings it to that state regardless of
starting point. It must also be safe to retry — a partial reconcile
that crashed midway resumes correctly on the next run, because
physical names are deterministic from stack/stage/logical-id and
the reconciler finds the existing resource instead of duplicating
it. The parameter contract (news/output/olds/bindings) and
the observe → ensure → sync shape are the provider’s concern — see
Provider › reconcile.
A second pass — convergence — re-runs reconcile for any
resource whose inputs changed because an upstream output changed
mid-deploy.
Replace
Section titled “Replace”Some property changes can’t be applied in place — for example,
changing a DynamoDB table’s partition key. The provider’s diff
returns { action: "replace" }, and alchemy:
- Creates a new resource with a new instance ID
- Updates downstream resources to reference the new resource
- Deletes the old resource
Because new and old coexist briefly, dependents get a clean cutover without downtime.
Changing a logical ID also plans a replacement — if you merely
renamed the resource, declare it with renamedFrom instead; see
Renaming Resources.
Delete
Section titled “Delete”provider.delete is called when a resource disappears from your
code, when a replacement supersedes it, or when you run
alchemy destroy. Like create, delete must be idempotent:
deleting an already-gone resource is a success, not an error.
alchemy destroy is just a plan where every persisted resource is
marked for deletion. Resources are removed in reverse dependency
order — dependents go first.
Plan: 2 to delete - Worker (Cloudflare.Worker) - Bucket (Cloudflare.R2.Bucket) Proceed? ◉ Yes ○ No ✗ Worker (Cloudflare.Worker) deleted ✗ Bucket (Cloudflare.R2.Bucket) deleted
Removal policy
Section titled “Removal policy”Each resource carries a removal policy that decides what happens to the physical cloud object when the resource is deleted — orphaned, destroyed, or superseded as a replacement’s old generation:
| Policy | What the engine does |
|---|---|
destroy |
Calls provider.delete, then drops the state row |
retain |
Skips provider.delete, then drops the state row |
Under retain, alchemy forgets the resource either way — only the
cloud object survives. Most resource types default to destroy; a few
whose contents are irreplaceable default to retain (e.g.
GitHub.Repository, Cloudflare.Zone).
Set the policy by piping a declaration through retain() or
destroy(). It applies to every resource declared inside the piped
effect, so it can decorate one resource or a whole scope:
import * as RemovalPolicy from "alchemy/RemovalPolicy";
Effect.gen(function* () { const stack = yield* Stack;
// never deleted by alchemy const uploads = yield* R2.Bucket("Uploads").pipe(RemovalPolicy.retain());
// retained in prod, torn down in every other stage const cache = yield* R2.Bucket("Cache").pipe( RemovalPolicy.retain(stack.stage === "prod"), );});The policy is a decoration, not a prop, so changing it produces no diff — the resource plans as a no-op and the plan reports no changes. The deploy still persists the new policy onto the state row (the orphan delete reads it from there, long after the declaration is gone), so a policy change takes effect from the very next deploy.
Idempotency and recovery
Section titled “Idempotency and recovery”State persistence can fail after the cloud operation succeeds — the
network drops between “bucket created” and “state saved”. Alchemy
handles this by requiring reconcile and delete to be safe to
retry:
- Reconcile: deterministic physical names plus the observe-step mean a retry finds the existing resource instead of creating a duplicate, and re-syncs any aspect that drifted.
- Delete: a missing resource is treated as already deleted.
- Read: providers can implement
readso alchemy can recover state from the live cloud when persistence fails partway, and to detect adoptable resources on a fresh state store.
Adoption
Section titled “Adoption”When planning a resource that has no prior state, the engine calls
provider.read (if the provider implements it). This serves two
overlapping purposes:
- State recovery — the resource was created on a previous
deploy, but state was lost between the cloud op succeeding and the
store persisting.
readfinds the live resource and the engine rebuildscreatedstate from its attributes. - Adoption — you’re deploying against existing infrastructure
you didn’t manage with Alchemy yet (or you wiped state
intentionally).
readrecognizes the resource and the engine imports it into the new state.
A resource read recognizes as ours is adopted silently. One that
exists but isn’t ours fails with OwnedBySomeoneElse — re-running
with --adopt (or scoping the effect with adopt(true)) unlocks
the takeover. How providers signal ownership through read’s
return value is part of the provider contract — see
Provider › read. For the CLI flag, see
Adopting Resources.
Errors
Section titled “Errors”- Retryable errors (eventual consistency, dependency races) are retried automatically with backoff.
- Non-retryable errors (validation, authorization) fail immediately and surface in the plan output.
- Partial failures are safe to re-run thanks to idempotency.
Driving the lifecycle from the CLI
Section titled “Driving the lifecycle from the CLI”The same engine powers all of these commands:
| Command | What it does |
|---|---|
alchemy plan |
Run plan, print diff, exit |
alchemy deploy |
Plan, prompt for approval, apply |
alchemy destroy |
Plan with everything marked deleted, apply |
alchemy dev |
Plan + apply continuously on file changes |
See the CLI reference for the full set of flags
(--yes, --force, --dry-run, --stage, --profile, …).
Every lifecycle operation on this page is implemented per resource type by a Provider.
Where next
Section titled “Where next”- Renaming Resources — migrate state across a logical ID change instead of replacing. Next page.
- Providers — the object that implements
reconcile,delete,diff, andreadfor a resource type. - CLI — the commands and flags that drive the lifecycle.
- State Store — where the persisted state behind the plan lives.