Bidsmith
Declarative tooling for Google Ads campaigns. Think Terraform, but pointed at
Google Ads instead of cloud infrastructure: HCL2 config files, schema
validation, plan/apply against the live account.
The engine is deterministic. AI sits on top — authoring .bid files,
reviewing PRs, recommending optimizations — and the engine's behavior never
depends on a model version.
Status
Pre-alpha. Phase 1 (local parsing & validation) is partially done; nothing
talks to the Google Ads API yet.
| Verb | Status | What it does |
|---|
validate | partial | Parse .bid, check schema and references |
export | partial | Render a .bid from a JSON description (testing aid) |
fmt | stub | Canonicalize .bid files |
plan | stub | Diff .bid vs. live, validate-only via API |
apply | partial | Show the plan, prompt for yes, then mutate (--auto-approve skips the prompt) |
pull | partial | Dump the live account as raw SearchStream JSON (round-trips through export --from-gads-search-response) |
refresh | stub | Pull live state into .bid |
Resource coverage today: provider "google_ads",
google_ads_campaign_budget, google_ads_campaign (SEARCH with
manual_cpc / network_settings), google_ads_ad_group,
google_ads_ad_group_ad (with responsive_search_ad, including a
list-attribute form for headlines / descriptions),
google_ads_ad_group_criterion (single keyword or bulk
keyword {} / negative_keyword {} sub-blocks),
google_ads_campaign_criterion (keyword / location / language /
proximity, plus bulk negative_keyword {} sub-blocks),
google_ads_shared_set and google_ads_campaign_shared_set for
reusable negative-keyword lists shared across campaigns,
google_ads_conversion_action, google_ads_call_asset,
google_ads_customer_asset. See ROADMAP.md for
what's queued next.
Example
A minimal .bid file:
provider "google_ads" {
customer_id = "1234567890"
login_customer_id = "9876543210"
}
resource "google_ads_campaign_budget" "summer" {
name = "Summer 2026"
amount_micros = 10000000
delivery_method = "STANDARD"
}
resource "google_ads_campaign" "summer_search" {
name = "Summer 2026 — Search"
status = "PAUSED"
advertising_channel_type = "SEARCH"
campaign_budget = google_ads_campaign_budget.summer.id
manual_cpc {
enhanced_cpc_enabled = false
}
}
Validate it:
$ bidsmith validate examples/basic
OK: 1 file(s) valid.
When there's a problem, errors are source-mapped (miette-rendered):
× invalid value "TURBO"; expected one of [STANDARD, ACCELERATED]
╭─[examples/broken/schema.bid:8:21]
7 │ amount_micros = "ten million"
8 │ delivery_method = "TURBO"
· ───────
9 │ }
╰────
Compact forms for repetitive resources
Real campaigns accumulate dozens of keywords and negatives. Prefer the
bulk and list-attribute forms — they're equivalent to the one-resource-
per-keyword shape but a lot shorter:
resource "google_ads_ad_group_criterion" "warszawa_phrases" {
ad_group = google_ads_ad_group.warszawa.id
status = "ENABLED"
keyword { text = "klimatyzacja Warszawa", match_type = "PHRASE" }
keyword { text = "montaż klimatyzacji Warszawa", match_type = "PHRASE" }
keyword { text = "klimatyzator inwerterowy Wwa", match_type = "PHRASE" }
}
resource "google_ads_shared_set" "competitor_brands" {
name = "Klima — competitor brands"
status = "ENABLED"
negative_keyword { text = "samsung", match_type = "BROAD" }
negative_keyword { text = "lg", match_type = "BROAD" }
negative_keyword { text = "daikin", match_type = "BROAD" }
}
resource "google_ads_campaign_shared_set" "warszawa_brands" {
campaign = google_ads_campaign.warszawa.id
shared_set = google_ads_shared_set.competitor_brands.id
}
resource "google_ads_ad_group_ad" "warszawa_rsa" {
ad_group = google_ads_ad_group.warszawa.id
ad {
final_urls = ["https://example.com/warszawa/"]
responsive_search_ad {
headlines = [
{ text = "Klimatyzacja Warszawa", pin = "HEADLINE_1" },
"Cicha praca, niski prąd",
{ text = "Bezpłatna wycena", pin = "HEADLINE_3" },
]
descriptions = [
"Montaż klimatyzacji split i multi w Warszawie.",
"Działamy w Warszawie i okolicach.",
]
}
}
}
See examples/bulk/main.bid for the full
campaign — re-encoding a 1025-line one-resource-per-keyword campaign
in these forms gives 177 lines.
Install
brew install chrmod/tap/bidsmith