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.searchblack-friday-2026.google_ads_campaign.searchWithout 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.searchsummer-2026.google_ads_campaign.searchThis 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.idbidsmith resolves it like this:
- First, look for
google_ads_campaign_budget.summerin the same module (summer-2026.bid). - If not found, search every other module.
- If exactly one match exists, use it.
- 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.bidfor things that span the whole account: theproviderblock, 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:
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
.bidfile, 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=valuedoes 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.searchreferences 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
moduleblock. The values must be plain literals (text, numbers, true/false) matching the template’svariabletypes. - Shared attributes on the block apply to every instance. Above,
every campaign targets the same
geo; onlycampaign_nameandfinal_urldiffer. 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.variantsworks whenlocal.variantsis 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. Usefor_eachwith a map today.output "x" { value = … }and cross-module references.- Directory sources (multiple
.bidfiles per module) and GitHub source resolution. - Nested modules (a module file containing its own
moduleblock).
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.