Skip to content

Repeat a resource for each item in a list

Some resources come in near-identical packs: the mobile + tablet exclusion pair your account mandates on every campaign, or eleven sitelink attachments that differ only in which asset they point at. Writing each one out by hand is pure boilerplate — and the copies drift.

for_each on a resource block fans one block out into one resource per entry.

From a list

Each entry becomes each.value (and its own address key). The classic device-exclusion pair:

resource "google_ads_campaign_criterion" "cookies_device_exclusions" {
for_each = ["MOBILE", "TABLET"]
campaign = google_ads_campaign.cookies.id
bid_modifier = 0
device {
type = each.value
}
}

That is two live criteria — addressed as cookies_device_exclusions["MOBILE"] and cookies_device_exclusions["TABLET"] — from one block. The list can also live in a shared local (for_each = local.excluded_devices) so every campaign file reuses the same one.

From a map

When each copy needs its own value, use a map: the key names the instance, each.value carries its value — including references to other resources:

resource "google_ads_campaign_asset" "cookies_sitelinks" {
for_each = {
neverconsent = google_ads_sitelink_asset.sl_neverconsent.id
adblock = google_ads_sitelink_asset.sl_adblock.id
help = google_ads_sitelink_asset.sl_help.id
}
campaign = google_ads_campaign.cookies.id
asset = each.value
field_type = "SITELINK"
}

each.key works too, anywhere in the block — including inside ${…} interpolation, so a name like "Sitelink — ${each.key}" names each instance after its map key.

When each copy needs more than one value

A map value can be a whole record, and each.value.<field> reaches inside it. That is what turns N sitelinks — four fields each — into one block:

resource "google_ads_sitelink_asset" "sl" {
for_each = {
howto = { text = "How it works", url = "https://example.com/how" }
chrome = { text = "For Chrome", url = "https://example.com/chrome" }
}
link_text = each.value.text
final_urls = [each.value.url]
}

Misspell a field and bidsmith says which names the entry actually has, rather than leaving you to compare two lists by eye.

Attaching what you just fanned out

A generated instance has a keyed address — sl["howto"] — and you can point at it from another resource with the same key:

resource "google_ads_campaign_asset" "sl_link" {
for_each = ["howto", "chrome"]
campaign = google_ads_campaign.cookies.id
asset = google_ads_sitelink_asset.sl[each.key].id
field_type = "SITELINK"
}

So the assets and their attachments both collapse to one block, driven by the same set of keys.

Adopting it on a live account

Rewriting existing hand-written resources into a for_each block is safe: criteria, assets, and keywords are matched by their content, so bidsmith plan shows no changes. Campaigns, ad groups, and ads keep their live state too — their identity label just moves to the new keyed address, which plan shows as an ~ adopt (label only) row before anything is written.

Where it stops

for_each fans out the resources under a campaign. To stamp out whole campaign families — same structure, different city or product — use a module with for_each instead.

For callouts and structured snippets specifically, there is now a shorter road still: declare them straight on the campaign and skip the asset and attachment resources altogether. Reach for for_each when the assets are shared between campaigns, or when they carry fields the inline form doesn’t cover.