Responses to pipeline events
Optional: hand this page to your coding agentThe steps work by hand too.Show the whole prompt
Read https://intentius.io/terragucci/reference/responses/.
Set `respond:` in terragucci.yml only for the events I name, run `npx terragucci config check`, and open a pull request.
Run `terragucci respond` in its default dry-run mode only, and show me what each response would do.
Never apply, approve (a pull request review or `terragucci approve`), override a policy denial (`terragucci override`), use `--mode apply`, or merge; never touch `.chant/allowed_signers` or `chant/lifecycle`.Every event has a default response that needs no model. No response runs a coding agent; people approve, apply and merge.
| Event | Response (default) | Other choices |
|---|---|---|
| plan finished | the grouped summary as the pull-request note | none |
| wave refused | a root-by-root diff of the approved plan against the current one, naming the attributes that moved | off |
| apply failed | triage from a table of known provider errors, each with its likely fix | off |
| drift found | a pull request writing the live value where the root sets a literal, and import blocks for resources the state does not hold | attribute, which also names who changed each drifted attribute; off |
| tip raised | one small pull request per tip: pin a provider from its lock file, add a lock file, add a canary | off |
| format check failed | a fmt commit on the pull request’s branch, on request |
off |
| module published | release notes from conventional commits | off |
| rollout wave merged | with rollouts set, a scheduled job opens the next wave once the merged one applied |
off, which leaves the job out |
| module changed with no conventional commit | none; tf-publish proposes a patch |
suggest: a suggested bump in a release pull request, from the typed-decision service |
| pull request opened or updated | none | check: a typed decision flags a description that leaves out what the plan destroys or replaces |
Settings
Section titled “Settings”Set a response per event under respond in terragucci.yml; a key you leave out keeps its default.
respond:
drift: pull-request # the default; attribute names who changed each attribute
apply-failed: triage # the default
plan: summary| Key | Takes | Default |
|---|---|---|
plan |
summary |
summary |
wave-refused |
diff, off |
diff |
apply-failed |
triage, off |
triage |
drift |
pull-request, attribute, off |
pull-request |
tips |
pull-request, off |
pull-request |
fmt |
commit, off |
commit |
publish |
notes, off |
notes |
rollout |
next-wave, off |
next-wave |
version-bump |
suggest, off |
off |
description |
off, check |
off |
Commands
Section titled “Commands”Each response is a command that dry-runs by default; --mode apply opens the pull request or pushes the commit.
terragucci respond plan --report terragucci-report
terragucci respond wave-refused --approved terragucci-report/approved --current terragucci-report/current --wave 2
terragucci respond apply-failed --log apply.log
terragucci respond drift --mode apply
terragucci respond drift --root envs/prod/orders --import aws_sqs_queue.extra=https://sqs.us-east-1.amazonaws.com/123456789012/extra --mode apply
terragucci respond tips --mode apply
terragucci respond tips --report terragucci-report --branch my-change --mode apply
terragucci respond fmt --branch my-change --mode apply
terragucci respond publish --module modules/network
terragucci respond version-bump --module modules/network --mode apply
terragucci respond rollout modules/network 1.4.0 --mode apply
terragucci respond rollout --mode applyWith no module, respond rollout finds the rollouts in flight from their pull requests’ terragucci/rollout/ branches and continues each. Without rollouts set, nothing opens the next wave until someone runs it.
| A rollout’s newest wave | respond rollout |
|---|---|
| merged and applied | opens the next wave |
| merged, not yet applied | waits |
| still open | leaves it alone |
| closed without merging | leaves it alone |
| the last wave, merged | leaves it alone |
--json prints one envelope, as every other command does.
Refused waves and failed applies
Section titled “Refused waves and failed applies”Wave refused
Section titled “Wave refused”An approval binds a wave’s set digest, so a wave whose root plan changed after approval applies nothing. The diff reads terragucci-report/approved and terragucci-report/current and lists each root whose plan digest moved. Approve again only once the new plan is the one you want.
Apply failed
Section titled “Apply failed”Triage knows these provider error classes:
| Class | Matches | Likely fix |
|---|---|---|
| access denied | AccessDenied, AccessDeniedException, UnauthorizedOperation |
grant the permission the message names to the apply role |
| quota | VpcLimitExceeded, LimitExceeded, TooManyBuckets, ServiceQuotaExceededException |
raise the quota or remove unused resources |
| throttling | Throttling, ThrottlingException, RequestLimitExceeded |
run again; lower -parallelism if it repeats |
| already exists | EntityAlreadyExists, ResourceAlreadyExistsException, BucketAlreadyOwnedByYou, InvalidGroup.Duplicate, QueueAlreadyExists |
import it, or give it another name |
| dependency | DependencyViolation, DeleteConflict, BucketNotEmpty |
remove what depends on it first |
| state lock | Error acquiring the state lock |
wait; a person runs force-unlock if no run holds it |
The table covers AWS provider errors and Terraform’s state lock message; any other provider’s error is listed as unknown.
A refresh-only plan finds the drift (a full plan on a choudoufu root under live resource markers), and the pull request writes the live value where the root’s own resource block holds a literal. Merging it accepts the outside change, so read it first.
Other changes are only reported:
| The value | Why it stays |
|---|---|
| set from a variable or an expression | the literal to change is elsewhere |
| set inside a module | the module serves other roots too |
on a count or for_each instance |
one literal sets every instance |
| never set in the root | it comes from a default |
| on a resource deleted outside Terraform | the next apply makes it again |
In a Terragrunt repo a unit’s resources are in the module its terraform.source names, and what differs between units is their inputs. Where the module sets the drifted attribute to var.<name> and the unit’s own terragrunt.hcl sets <name> to a literal in inputs, the pull request writes the live value there, so only that unit changes. A literal in the module is only reported, since every unit calling the module shares it. The same goes for a module from outside the repo. The drift job plans again only the units its report shows drifted. --import names a root’s files, so it is refused in a Terragrunt repo.
Under synth the roots are the app’s output, so there is no literal to write: respond.drift: pull-request with a drift schedule is a config error, and respond drift refuses.
For a resource the state lacks, pass --import <address>=<id>. From Terraform 1.14.1, a root with a .tfquery.hcl file gets imports from terraform query.




Drift attribution
Section titled “Drift attribution”With respond.drift: attribute, each drifted attribute goes through three steps in order; the first to answer wins.
| Step | Source | Decides |
|---|---|---|
| 1 | a table of by-design writes: ECS task count, autoscaling and node group sizes, provisioned DynamoDB capacity, RDS minor upgrades, aws: tags |
the write is expected |
| 2 | CloudTrail LookupEvents over 14 days, for aws_ resources |
the newest write Terraform did not make: a service principal is a controller, a user is a person |
| 3 | a typed decision, when decide is set |
the attribute stays unattributed below the choice threshold; sensitive values are never shown |
Reading the audit log
Section titled “Reading the audit log”| Needs | Detail |
|---|---|
the aws CLI, version 2 |
the generated drift job installs it against a pinned SHA-256 unless it is on the path; a pipeline you write installs it the same way |
cloudtrail:LookupEvents |
on the job’s role |
| the region | audit_region, else the CLI’s |
| roots or Terragrunt units | terragucci stage tf-drift lists who changed each attribute in the drift issue, under its root or unit |
decide.token_env |
received on GitHub and Forgejo |
If step 2 cannot run, it says so and step 3 follows.


| Attributed to | In the drift pull request | Suggestion |
|---|---|---|
| a controller write | left out | add lifecycle { ignore_changes = [...] } to the resource |
| a provider default change | left out | pin the provider version that kept the old value, or set the attribute |
| a human edit | kept, with the actor when known | accept or revert it in the pull request |
Nothing here applies or merges.
Pull request and release events
Section titled “Pull request and release events”Description check
Section titled “Description check”When respond.description: check and a decide block are set, the plan job asks the service whether the title and description fit the plan. The flag never blocks a merge and changes no gate.
| Outcome | The note | intent.json |
|---|---|---|
| a yes at or above the threshold | starts with a line naming the destroys and replacements the text omits | records the decision |
| anything else | unchanged | says why |
terragucci respond description --report terragucci-report --title "retag email" --description "Tags only." --mode applyTips, fmt and release notes
Section titled “Tips, fmt and release notes”| Tip | The pull request |
|---|---|
| a provider taken by a range | pins it in required_providers at the version the lock file holds, so the next plan changes nothing |
| a root with no lock file | adds one written by providers lock, with hashes for Linux and macOS on amd64 and arm64 unless --platform names others |
| no canary | adds waves.canary |
a rename (--report) |
adds a moved block for each resource the plan destroys and creates with the same configuration (terragucci-moved), after the new block, in a pull request into --branch |
tips --report reads the plans of a stage tf-plan report and proposes only the moved blocks; run it in a checkout of the branch that renamed the resources, after its plan. The generated tips job runs on the default branch after the applies, without --report.
Under synth the tips job runs the command first, and only the canary tip opens: the other two would edit files the command writes.


fmt runs on request and pushes one commit to the pull request’s branch, and refuses the default branch. In a Terragrunt repo it runs terragrunt hcl fmt beside the binary’s fmt, and the commit is style: terragrunt hcl fmt when only .hcl files moved. The generated pipeline’s fmt job runs it after a failed check, so the check job holds no token that pushes.


Release notes cover the commits that touched a module between its last two tags, breaking changes first.
Version bump
Section titled “Version bump”The bump comes from the version bump rules. Under version-bump: suggest, the typed-decision service picks the bump when no conventional commit landed since the last release; this needs decide.
| Where | What |
|---|---|
| a module’s releases | its git tags, <module>/v1.2.0 |
| a registry-only project | pass --since <ref>; the version file at that ref is the last release (0.0.0 if none) |
the version-bump job |
runs terragucci respond version-bump --mode apply on the default branch after every apply wave, with full history; it needs permission to push a branch and open a pull request; a failed response never fails the job |
| the release pull request | writes the version file with the bump and its probability; merging confirms it and the next tf-publish publishes it |


Runtime
Section titled “Runtime”Responses run in CI with no model. To hand a result to your own agent, run the command with --json in a job whose token can only comment, as in Have an agent summarize a refused wave. The agent never holds an apply role or a gate’s signing key.
These docs count page views and clicks with PostHog. They set no cookies, store nothing in your browser, and send nothing when your browser asks not to be tracked.