Skip to content

Modules

In bidsmith, each .bid file is implicitly a module. The filename (without the .bid extension) is the module name. This matters because resource addresses include the module:

A resource declared in summer-2026.bid as:

resource "google_ads_campaign" "search" {
...
}

…has the full bidsmith address summer-2026.google_ads_campaign.search.

You’ll see this address in three places: plan and apply output, error messages, and (eventually) state labels stored on Google Ads itself.

Why it matters

The module prefix lets you have two campaigns named search — one in summer-2026.bid, another in black-friday-2026.bid — and bidsmith can tell them apart:

summer-2026.google_ads_campaign.search
black-friday-2026.google_ads_campaign.search

Without module prefixes you’d have to invent globally unique names like summer_2026_search and black_friday_2026_search. Module prefixes give you that for free.

Single-file projects

If your project has exactly one file (main.bid), then every address is prefixed with main. In one-file projects you almost never see this — the prefix doesn’t add information when there’s only one possibility. bidsmith hides it from the output when every resource in a plan lives in the same module.

Multi-file projects

Once you have more than one file, the prefix appears in output:

black-friday-2026.google_ads_campaign.search
summer-2026.google_ads_campaign.search

This is intentional — it disambiguates which file the resource came from. If you find yourself squinting at the prefix in plan output, that’s the prefix earning its keep.

How references work across modules

When resource A in summer-2026.bid says:

campaign_budget = google_ads_campaign_budget.summer.id

bidsmith resolves it like this:

  1. First, look for google_ads_campaign_budget.summer in the same module (summer-2026.bid).
  2. If not found, search every other module.
  3. If exactly one match exists, use it.
  4. If multiple modules have a google_ads_campaign_budget.summer, bidsmith refuses to guess — it raises an error and tells you which modules have a candidate. Rename one of them or scope the reference explicitly.

When to split files

There’s no rule. Some heuristics:

  • One file per campaign. Works well when campaigns are long-lived and roughly independent (different products, different markets).
  • One file per market. Works well when you run the same campaigns in different countries.
  • A dedicated account.bid for things that span the whole account: the provider block, conversion actions, shared negative lists. Then per-campaign files for the campaigns themselves.
  • Don’t over-split. A file with one resource in it is usually too small. Combine related things.

The right answer is whatever makes your git diff legible six months from now.

Explicit module blocks

When you want to instantiate the same shape multiple times — say, a “city campaign” repeated for Warsaw and Kraków — use a top-level module block. It points at a .bid file and supplies values for that file’s variable blocks:

module "warsaw" {
source = "./modules/city-campaign.bid"
city_name = "[W1] Warsaw — Search"
latitude = 52.229675
longitude = 21.012228
}
module "krakow" {
source = "./modules/city-campaign.bid"
city_name = "[W1] Krakow — Search"
latitude = 50.064650
longitude = 19.944980
radius_km = 20
daily_budget_micros = 8000000
}

The module file describes the shape — its variables are the inputs you can dial per instance:

modules/city-campaign.bid
variable "city_name" { type = string }
variable "latitude" { type = number }
variable "longitude" { type = number }
variable "radius_km" { type = number, default = 15 }
variable "daily_budget_micros" { type = number, default = 10000000 }
resource "google_ads_campaign" "search" {
name = var.city_name
...
}

Resources inside module "warsaw" get the address warsaw.google_ads_campaign.search; inside module "krakow", krakow.google_ads_campaign.search. The module block’s instance name replaces the file stem.

What the source attribute accepts

  • A path to a single .bid file, relative to the calling file’s directory: source = "./modules/city-campaign.bid".

That’s it for v1. Directory sources, GitHub URLs (github.com/org/repo), and registry references will land later — directory sources first.

Scoping rules

Each module instance is an isolation boundary:

  • Variables in the module are filled by the module block’s attributes (+ defaults). --var name=value does not flow into modules.
  • Locals declared inside the module are private to the module.
  • Resources inside the module can reference each other via the usual <type>.<name> syntax.
  • Resources outside the module cannot reach inside (no module.warsaw.google_ads_campaign.search references yet), and vice versa. If two cities need to share a budget, declare it at the top level and pass its id through a variable.

One thing does cross the boundary: defaults blocks declared outside your modules apply inside them too, so a template can write defaults = defaults.search_shell and the shell stays in one place for the whole tree. A defaults block inside a template is private to that template and shadows an outer block of the same name.

Repeat a module with for_each

Writing one module block per variant gets tedious fast. When you have a table of near-identical campaigns — same shape, a few values changing — give a single block a for_each map instead. Each entry in the map becomes one instance:

module "ghostery_search" {
source = "./templates/preroll-campaign.bid"
geo = "geoTargetConstants/2840" # shared by every instance
for_each = {
privacy = {
campaign_name = "Ghostery — Privacy"
final_url = "https://www.ghostery.com/?utm_campaign=search_privacy"
}
adblock = {
campaign_name = "Ghostery — Ad Blocker"
final_url = "https://www.ghostery.com/?utm_campaign=search_adblock"
}
trackers = {
campaign_name = "Ghostery — Anti-Tracking"
final_url = "https://www.ghostery.com/?utm_campaign=search_trackers"
}
}
}

This builds three campaigns from one template. A few rules:

  • The map keys name the instances. Resources land at ghostery_search.privacy.google_ads_campaign.search, ghostery_search.adblock.…, and so on — the key slots in between the block label and the resource address.
  • Each entry’s object supplies that instance’s inputs, exactly like the attributes on a plain module block. The values must be plain literals (text, numbers, true/false) matching the template’s variable types.
  • Shared attributes on the block apply to every instance. Above, every campaign targets the same geo; only campaign_name and final_url differ. If an entry also sets a shared key, the entry wins for that instance.
  • The table itself can come from a local. for_each = local.variants works when local.variants is a map — handy when the same table drives more than one block.

Renaming a clone-by-hand setup into a for_each template is safe against your live account: bidsmith matches live resources by their content (campaign name, keywords, geo…), not by their address, so the campaigns aren’t recreated even though their addresses change. See examples/modules-for-each/.

What’s still deferred

  • count — instantiating a block a fixed number of times. Use for_each with a map today.
  • output "x" { value = … } and cross-module references.
  • Directory sources (multiple .bid files per module) and GitHub source resolution.
  • Nested modules (a module file containing its own module block).

If your use case bumps into any of these, open an issue.

Next

  • Variables — how a module’s inputs are typed and consumed.
  • References — the mechanics of resource-to-resource pointers.
  • The .bid file — what’s inside a single file.