Back to Blog
Terragrunt

Terragrunt: How to use the autoinclude block

Yousif Akbar
Yousif
Akbar
,
Principal Software Engineer
July 27, 2026

Terragrunt 1.1 graduated the stack-dependencies experiment to general availability, and the headline mechanism is the autoinclude block.

The TL;DR: you can now declare how units in a generated stack wire together directly in the terragrunt.stack.hcl file that composes them, instead of threading dependency paths through your catalog with dependency blocks defined in each unit's terragrunt.hcl.

If you compose infrastructure from stacks, especially from a catalog of reusable units and stacks, this gives you more tools to decide where wiring lives. Dependency declarations can move out of the catalog and into the stack file, which is the layer that usually has better context of surrounding infrastructure. The same mechanism also lets you patch any part of a unit or nested stack definition at generation time.

The problem: wiring units in a generated stack

A Terragrunt stack generates a tree of units from a single terragrunt.stack.hcl file. Each unit block points at a unit configuration, often in a catalog repository, and terragrunt stack generate copies that configuration into a .terragrunt-stack directory where it runs.

Until stack dependencies landed, the stack file had no mechanism for declaring how its generated units depended on each other. The dependency block lived in the unit's terragrunt.hcl, which is catalog code, and the catalog has no idea where its consumers will place sibling units. The standard workaround was to plumb paths through values:

# live/my-stack/terragrunt.stack.hcl

unit "vpc" {
  source = "github.com/acme/catalog//units/vpc"
  path   = "vpc"
}

unit "app" {
  source = "github.com/acme/catalog//units/app"
  path   = "app"

  values = {
    vpc_path = "../vpc"
  }
}

# catalog/units/app/terragrunt.hcl

dependency "vpc" {
  config_path = values.vpc_path
}

inputs = {
  vpc_id = dependency.vpc.outputs.vpc_id
}

It works, but isn't ideal. Every catalog unit has to anticipate every way it might be wired, so the catalog accumulates dependency blocks and values inputs that only exist to serve particular compositions. And the paths are hand-written strings: rename a unit's path in the stack file and nothing tells you "../vpc" three lines down is now stale.

Wiring belongs to the composition, and the stack file has always known how its units fit together. There was just no way to say so there: the dependency block only worked in the unit's own terragrunt.hcl.

What autoinclude does

The autoinclude block goes inside a unit or stack block in terragrunt.stack.hcl files. At generation time, Terragrunt takes its contents and writes them to a partial configuration file alongside the generated configuration:

  • For units, the partial is written as terragrunt.autoinclude.hcl, next to the generated terragrunt.hcl.
  • For nested stacks, it's written as terragrunt.autoinclude.stack.hcl, next to the generated terragrunt.stack.hcl. The .stack.hcl suffix mirrors the file it patches, so tooling can tell what the file is for from its name alone.

Whenever Terragrunt parses a unit or stack, it automatically merges any autoinclude file in the same directory into the configuration. The catalog source stays untouched, but additional configuration is generated alongside it to define any wiring between components.

Two new variables make the wiring declarative: unit..path and stack..path reference components by their block name. Terragrunt resolves them to relative paths in the generated file, so renaming a path attribute propagates everywhere it's referenced.

Walkthrough: wiring a VPC to an app

Here's a simple example, an app unit that needs a VPC's ID:

# live/my-stack/terragrunt.stack.hcl

unit "vpc" {
  source = "github.com/acme/catalog//units/vpc"
  path   = "vpc"
}

unit "app" {
  source = "github.com/acme/catalog//units/app"
  path   = "app"

  autoinclude {
    dependency "vpc" {
      config_path = unit.vpc.path
    }

    inputs = {
      vpc_id = dependency.vpc.outputs.vpc_id
    }
  }
}

Run terragrunt stack generate and the .terragrunt-stack directory contains the two units as usual, plus one new file next to the app's terragrunt.hcl:

# live/my-stack/.terragrunt-stack/app/terragrunt.autoinclude.hcl

# Generated by Terragrunt from autoinclude block. Do not edit manually.

dependency "vpc" {
  config_path = "../vpc"
}

inputs = {
  vpc_id = dependency.vpc.outputs.vpc_id
}

The dependency block syntax is identical to what you'd write directly in a terragrunt.hcl; autoinclude just changes where it's authored.

The rule for what resolves when is simple: everything in the body is evaluated at generation time, in the context of the stack file. unit..path, stack..path, local.*, values.*, and function calls all land in the generated file as literals, which is why unit.vpc.path came out as "../vpc". The exception is dependency.*, carried over verbatim, because those outputs don't exist until the unit that produces them has run.

That context matters for functions: they evaluate against the stack file, not the unit. get_terragrunt_dir() returns the directory holding the terragrunt.stack.hcl, and path_relative_to_include() returns ".".

When you run terragrunt run --all -- plan, Terragrunt merges the autoinclude into the app's configuration, sees the dependency, orders the VPC first, and feeds its vpc_id output into the app's inputs, without any of this defined in the catalog's app unit.

Inside the block you can declare multiple dependency blocks, reference local values from the stack file, and build inputs from a mix of literals, locals, and outputs of several dependencies in one expression.

How the merge works

The autoinclude file merges into the unit's configuration the same way a regular include does by default: a shallow merge. Top-level keys from the unit and the autoinclude combine, and when both define the same key, the autoinclude wins and replaces the unit's value. An autoinclude body can't declare its own locals block; that fails generation. Declare locals in the stack file instead, where they resolve at generation time and reach the generated file as literals.

A terragrunt.autoinclude.stack.hcl injects unit and stack blocks into the generated terragrunt.stack.hcl. An injected block with a new name is added; an injected block whose name matches an existing one overrides it wholesale.

Note that a stack autoinclude can't declare a top-level dependency block, because stacks don't run and as such, they can't depend on anything. If a unit inside the stack needs a dependency, declare it in that unit's own autoinclude.

Patching beyond dependencies

The merge mechanism doesn't care whether the configuration you inject is a dependency block. Anything that's valid unit configuration can be used to patch the unit in the same way. Say one deployment of an app sits behind a flaky network path and needs retry behavior the catalog unit doesn't define:

# terragrunt.stack.hcl

unit "app" {
  source = "github.com/acme/catalog//units/app"
  path   = "app"

  autoinclude {
    errors {
      retry "transient_errors" {
        retryable_errors   = [".*Error: transient network issue.*"]
        max_attempts       = 3
        sleep_interval_sec = 5
      }
    }
  }
}

The retry configuration lands in the generated autoinclude file and merges into that one deployment of the unit. The catalog stays generic.

Nested stacks can be patched the same way. For example, dev and prod here consume the same environment stack from the catalog, but prod needs one extra unit.

# terragrunt.stack.hcl

stack "dev" {
  source = "github.com/acme/catalog//stacks/environment"
  path   = "dev"
}

stack "prod" {
  source = "github.com/acme/catalog//stacks/environment"
  path   = "prod"

  autoinclude {
    unit "security_scanner" {
      source = "github.com/acme/catalog//units/security-scanner"
      path   = "security-scanner"
    }
  }
}

Before this, the options were forking the environment stack for prod or teaching the catalog stack about a values flag for every conditional component. Now the divergence lives in the file that declares the environments, scoped to the environment that needs it.

Depending on whole stacks

dependency blocks can now target a stack directory, not just a unit. Set config_path to stack..path and the dependency exposes the aggregated outputs of every unit in the stack, addressed as dependency..outputs.., the same shape terragrunt stack output gives you.

# terragrunt.stack.hcl

stack "networking" {
  source = "github.com/acme/catalog//stacks/networking"
  path   = "networking"
}

unit "app" {
  source = "github.com/acme/catalog//units/app"
  path   = "app"

  autoinclude {
    dependency "networking" {
      config_path = stack.networking.path
    }

    inputs = {
      vpc_id = dependency.networking.outputs.vpc.vpc_id
    }
  }
}

In the run queue, a dependency on a stack expands to a dependency on all of its units: the app waits until everything in the networking stack has completed. The old workaround, reaching into ../networking/.terragrunt-stack/vpc with a hand-written path, coupled the composition to generated directory layout, and had to be rewritten by hand every time that layout changed.

The relationship only goes one way. Units can depend on stacks, but stacks can't depend on stacks or units. The reason for this is that stacks don't run; the units they generate do. As such, a stack has no place in the run queue to wait at.

Shared stack fragments

The include block now works in stack configurations, so a set of unit and stack declarations can be written once and pulled into several terragrunt.stack.hcl files. It's narrower than the unit include block: path is the only attribute, the included file can't declare locals or includes of its own, and the names it declares can't collide with the ones in the including file.

# terragrunt.stack.hcl

include "environment" {
  path = find_in_parent_folders("environment.stack.hcl")
}

unit "security_scanner" {
  source = "github.com/acme/catalog//units/security-scanner"
  path   = "security-scanner"
}

For Pipelines users, it's picked up automatically

Designed with Pipelines in mind

Gruntwork Pipelines decides which units in a generated stack need a plan or an apply by diffing the generated files on the filesystem. The autoinclude block was designed with that in mind: the wiring materializes as terragrunt.autoinclude.hcl files inside the generated tree, so a change to an autoinclude block produces a diff in exactly the units it touches.

Add a dependency to the app unit and Pipelines sees the app's autoinclude file change, so the app gets a run. The VPC's files didn't change, so it doesn't. No Pipelines configuration is involved; GitOps workflows run against only the components that need runs, the same way they do for any other change to a generated stack.

Things to know before adopting

  • You'll want Terragrunt 1.1 or later, where stack dependencies work with no flags. On 1.0.x, enable the experiment with --experiment stack-dependencies.
  • It's opt-in. Existing stack configurations keep working untouched, so you can add autoinclude blocks only where you want wiring in the stack file.
  • Units can depend on units and stacks, but stacks can't depend on stacks or units.
  • A dependency on a stack waits for every unit in that stack, and a stack is the smallest thing a dependency can target across a stack boundary, since path variables only see components declared in the same stack file.
  • The merge is shallow, and the autoinclude side wins on conflicts. An injected unit or stack block whose name matches an existing one replaces it wholesale rather than deep-merging.
  • It works well alongside the CAS and update_source_with_cas, which also graduated in 1.1: autoinclude handles the wiring between components while the CAS keeps their sources content-addressed and portable. The Terragrunt 1.1 release post covers both.

Wrapping up

Catalog units stay generic, and the stack file that composes them carries the wiring: dependencies, inputs, retries, per-environment patches. The generated tree holds all of it as ordinary files that Terragrunt merges when it parses each component.

The explicit stacks documentation has the full reference, and the Terragrunt 1.1 release post covers everything else that shipped alongside it.

If you have a stack passing dependency paths through values today, that's the one to convert first: move the wiring into autoinclude blocks, then drop the dependency blocks and values entries the catalog units no longer need.

Features like stack dependencies come out of the work we do supporting large Terragrunt estates. If that's the problem you're solving, check out Terragrunt Scale.

There's even a free tier where you can try it out today.