Effects#

A migration that ran. A cache that was invalidated. A notification that was sent. None of them leaves anything in the cloud to read back, so no plan can tell you whether it already happened.

A receipt is how you make it visible.

A receipt makes an invisible effect visible in the plan

What a receipt is#

An ordinary resource you declare, by convention an SSM parameter at /tofu-receipts/<estate>/<effect> holding a hash of the effect’s input.

A record store can be backed by Parameter Store too, so both can end up as parameters in the same account. The difference is who owns them. A record is written by choudoufu and its format is internal. A receipt is written by your configuration, appears in your plan, and is yours to read.

It goes through the ordinary plan and apply cycle. Its diff appearing in a plan is what tells a reviewer or a CI gate that this apply will trigger something outside the resources being managed.

choudoufu never runs the effect#

This is the part that makes the diff mean something. plan and apply touch the receipt resource and nothing else. The migration itself runs in the layer above, a CI step or a runbook, which sees the proposed receipt change, runs the real effect, and lets apply write the new value only once the effect succeeded.

If the tool ran the effect itself, the diff would stop being a preview of what is about to happen and become the thing happening mid-plan. That is a provisioner.

choudoufu does run provisioners, once an estate declares a record_store (before that it refuses them, because a failed one has nowhere to be remembered). But a provisioner is not a smaller receipt, and reaching for one here would answer a different question. A provisioner runs when its resource is created and never again; nothing about it is re-examined on a later plan, and no plan shows you that it is about to run. A receipt tracks staleness across a resource’s whole lifetime: its diff is the standing answer to “have this effect’s inputs changed since it last ran”, asked on every plan, for as long as the resource exists. Only one of those is reviewable before the fact.

The semantics are at-least-once. If the effect runs but the process dies before the receipt is written, the next plan proposes the same change and the effect runs again. Under-running never happens silently, and every unconfirmed effect stays visible as a pending diff.

choudoufu does not write receipts. It lints them, enforcing that the value is a hash or constant and never a SecureString, that nothing references a receipt’s attributes, and that inputs name secrets by pointer rather than by value.

Why a receipt is not a record#

Enforced rather than advised. A key_prefix whose first segment is tofu-receipts is a configuration error, so a record can never land in the receipts namespace.

Visibility is why. A receipt is AWS-native so its value stays readable with a plain aws ssm get-parameter, by someone with read-only IAM and no choudoufu binary. A record-store payload is tool-internal by design. Moving a receipt onto it would trade a one-line CLI call for choudoufu’s own JSON envelope, which is strictly worse for the one artifact whose job is being legible to someone not running the tool.

The tempting mistake#

Now terraform_data is record-backed, its triggers_replace looks like a pseudo-receipt. Do not use it that way. It hides the fingerprint in the tool’s own store instead of a declared resource, and collapses a receipt into “did an input change”, with no existence flavour, no hash flavour, and no naming convention the lint rules recognise.

terraform_data is for the graph, ordering an apply, feeding replace_triggered_by, or standing in for a resource that does nothing. Receipts are for external effects. Keep them apart.

live/RECEIPTS.md has the pattern and the reasoning behind each guard.