Skip to content

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. This page walks through each step, what triggers it, and how alchemy recovers when things go wrong. For the CLI flags that drive these operations, see the CLI reference.

new changed breaking removed unchanged Plan Reconcile (create) Reconcile (update) Replace Delete Noop

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.R2Bucket)

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.

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 state in news, it brings the cloud to that state regardless of starting point. It receives:

  • news — desired props
  • output — current attributes (undefined on greenfield, defined after a prior reconcile or after adoption)
  • olds — previous props (undefined on greenfield AND on adoption; defined only on routine updates)
  • bindings — resolved binding payload from upstream policies

A reconciler is shaped like observe → ensure → sync → return: read live cloud state via getX/describeX, create the resource if missing (catching AlreadyExists-style errors as races), then for each mutable aspect diff observed cloud state against desired and apply only the delta.

Because each step is independently idempotent, a partial reconcile that crashed midway resumes correctly on the next run. Physical names are deterministic from stack/stage/logical-id, so the “observe” step finds the previous reconcile’s output even if state persistence failed.

A second pass — convergence — re-runs reconcile for any resource whose inputs changed because an upstream output changed mid-deploy.

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:

  1. Creates a new resource with a new instance ID
  2. Updates downstream resources to reference the new resource
  3. Deletes the old resource

Because new and old coexist briefly, dependents get a clean cutover without downtime.

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.R2Bucket)

Proceed?
◉ Yes ○ No
 Worker (Cloudflare.Worker) deleted
 Bucket (Cloudflare.R2Bucket) deleted

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 read so alchemy can recover state from the live cloud when persistence fails partway, and to detect adoptable resources on a fresh state store.

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. read finds the live resource and the engine rebuilds created state from its attributes.
  • Adoption — you’re deploying against existing infrastructure you didn’t manage with Alchemy yet (or you wiped state intentionally). read recognizes the resource and the engine imports it into the new state.

Providers signal “this is mine” vs. “this exists but isn’t mine” via the Unowned(attrs) brand. The engine routes:

read returns--adopt off--adopt on
undefinedcreatecreate
owned (plain attrs)silent adoptsilent adopt
Unowned(attrs)fail OwnedBySomeoneElsetake over (silently)

See Provider › read for the implementation contract and CLI › Adoption for the CLI flag.

  • 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.

The same engine powers all of these commands:

CommandWhat it does
alchemy planRun plan, print diff, exit
alchemy deployPlan, prompt for approval, apply
alchemy destroyPlan with everything marked deleted, apply
alchemy devPlan + apply continuously on file changes

See the CLI reference for the full set of flags (--yes, --force, --dry-run, --stage, --profile, …).