Skip to content

google_ads_campaign

A campaign is the top-level container in Google Ads. It holds one or more ad groups, points at a single budget, picks a channel (search, display, shopping, etc.), and decides where ads can show.

Example

A search campaign with manual CPC bidding and Google-search-only targeting:

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
}
network_settings {
target_google_search = true
target_search_network = false
target_content_network = false
target_partner_search_network = false
}
}

Flight windows

start_date and end_date are YYYY-MM-DD strings, and they’re how a time-boxed campaign stops on its own:

resource "google_ads_campaign" "instream_test" {
name = "YouTube in-stream — August test"
advertising_channel_type = "VIDEO"
campaign_budget = google_ads_campaign_budget.instream.id
start_date = "2026-08-11"
end_date = "2026-08-25"
}

A campaign with no end_date runs until somebody stops it. That’s the default, and for an always-on brand campaign it’s the right one — but for a two-week test it’s the difference between a fortnight of spend and an open tab. Putting the end date in the file makes the intended envelope a committed fact that goes through review like everything else, instead of a reminder somebody has to remember.

Leave a date out and bidsmith won’t touch it: a file that names no flight window manages neither end of it. validate catches dates that aren’t real dates (2026-02-30, 11.08.2026) and warns when a campaign would end before it starts, which Google accepts and then silently never delivers.

Geo and language targeting

The languages and locations lists are the readable way to say who sees a campaign. Each entry expands to one positive campaign criterion under the hood, so a targeting change reads like plain English in a pull request — locations = ["US"]locations = ["US", "SG"] — instead of an opaque geoTargetConstants/2702.

resource "google_ads_campaign" "search_ublock" {
name = "Search_uBlock"
advertising_channel_type = "SEARCH"
campaign_budget = google_ads_campaign_budget.search_ublock.id
languages = ["en"]
locations = ["US", "GB", "CA"]
}
  • locations takes ISO 3166-1 alpha-2 country codes ("US", "GB", "SG", …). For a city, region, or anything finer-grained, drop in the raw geoTargetConstants/NNNN string in the same list — look the id up with Google’s geo target constant table.
  • languages takes short language codes ("en", "de", "pl", "zh-CN", …). Less common languages can be given as a raw languageConstants/NNNN string.

Two rules keep targeting unambiguous:

  • A campaign declares each axis once. Setting locations on the campaign and a positive google_ads_campaign_criterion with a location {} block pointing at it is a validation error — pick one source of truth.
  • The inline form is positive targeting only. Negative locations, proximity radii, and non-default statuses still use an explicit google_ads_campaign_criterion.

Because bidsmith matches live criteria by their resolved constant (not by an address), converting existing explicit location {} / language {} criteria to the inline form — or adopting targets that already exist on the account — plans as a no-op. refresh emits the inline form by default for plain positive targeting.

On a Demand Gen campaign the lists need one extra line: Google asks where the targeting lives — campaign or ad groups — before it accepts any. See demand_gen_campaign_settings below.

In a location, or interested in it

locations says which places a campaign targets. geo_target_type_setting says how Google reads them — and it’s the difference between showing ads to people in the United States and showing them to anyone in the world who searches for or watches content about the United States:

resource "google_ads_campaign" "us_instream" {
name = "YouTube in-stream — US"
advertising_channel_type = "VIDEO"
campaign_budget = google_ads_campaign_budget.instream.id
locations = ["US"]
geo_target_type_setting {
positive_geo_target_type = "PRESENCE"
negative_geo_target_type = "PRESENCE"
}
}
  • PRESENCE — people in (or regularly in) the location.
  • PRESENCE_OR_INTEREST — people in the location plus people anywhere else showing interest in it. This is Google’s default.

The default is the generous one, which matters most when a campaign’s whole identity is a market. Run a US, a German, and a French version of the same test on PRESENCE_OR_INTEREST and all three can serve to the same third-country audience — the per-market numbers stop being comparable, and the reason isn’t visible anywhere in the campaign. Say PRESENCE in the file and that’s settled, reviewed, and re-checked on every apply. Google’s advanced location options cover the same setting from the UI side.

Leave the block out and bidsmith won’t touch either side — the campaign keeps whatever the account has. Set one and leave the other out and only the one you named is managed; the negative side governs the same in-versus-interested-in question for locations you exclude.

Device targeting

devices says which kinds of screen a campaign is for. A desktop-only product is one line:

resource "google_ads_campaign" "search_ublock" {
name = "Search_uBlock"
advertising_channel_type = "SEARCH"
campaign_budget = google_ads_campaign_budget.search_ublock.id
devices = ["DESKTOP"]
}

devices is a closed list: whatever it leaves out is switched off. The line above targets desktop and stops mobile and tablet from serving.

That is not the same as deleting them. Google creates a criterion for every device type on every campaign and the API refuses to remove one, so “not targeted” is spelled as a bid modifier of zero — the ad is eligible in name only and never wins an auction. bidsmith writes those zeros for you.

If you would rather zero specific devices and stay silent about the rest, excluded_devices is the open form:

excluded_devices = ["MOBILE", "TABLET"]

It zeroes exactly what it names and invents no criterion for anything else. Declaring both on one campaign is a validation error — devices already excludes what it omits, so a second list can only repeat it or contradict it.

Three notes:

  • Only MOBILE, DESKTOP, and TABLET are implied by omission. CONNECTED_TV (video campaigns) and OTHER are never switched off behind your back — name them in devices to target them.
  • A bid adjustment is not an exclusion. bid_modifier = 0.8 on mobile means “bid less here”, which no spelling of these attributes can express; keep it as an explicit google_ads_campaign_criterion. refresh leaves such campaigns in the explicit form.
  • As with locations and languages, a campaign declares the axis once. Setting devices and a device {} criterion pointing at the same campaign is a validation error.

Converting an existing device trio to the inline form plans as a no-op: bidsmith matches live criteria by device type, not by address.

Callouts and snippets, declared where they’re used

A callout asset is its text. Saying so used to take three resources — the asset, the attachment, and the field_type naming what the attachment already implied. Declare them on the campaign instead:

resource "google_ads_campaign" "fbads" {
name = "GH_FacebookAds"
advertising_channel_type = "SEARCH"
campaign_budget = google_ads_campaign_budget.fbads.id
callouts = [
"Blocks feed & sidebar ads",
"Open source & private",
"Free forever",
]
structured_snippet {
header = "Types"
values = ["Ad blocker", "Tracker blocker", "Pop-up blocker"]
}
}

bidsmith creates the assets and attaches them — the account ends up in exactly the state the long form produced, and adopting an account that already has them plans as a no-op, because assets are matched by content rather than by address.

Ad groups take the same two, for extensions that belong to one keyword theme rather than the whole campaign.

Reach for google_ads_callout_asset and google_ads_campaign_asset when an asset is shared — attached to more than one campaign, to an ad group, or to the account. That is the one thing the inline form cannot express, so refresh leaves shared assets in the resource form and folds only the ones a single campaign owns.

Tracking the click, not the landing page

final_url_suffix is a query string Google appends to the landing page at click time, and custom_parameters fills in the {_name} placeholders it can reference:

resource "google_ads_campaign" "cookies" {
name = "GH_Cookies — Search"
advertising_channel_type = "SEARCH"
campaign_budget = google_ads_campaign_budget.cookies.id
final_url_suffix = "utm_source=search&utm_campaign=GH_Cookies_0708-{_slug}"
custom_parameters = { region = "eu" }
}

Both fields exist on the campaign, the ad group, and the ad body, and the most specific one wins — so a UTM convention lives on the campaign and each ad supplies only the part that varies.

Two things worth knowing before you move a UTM query out of final_urls:

  • The suffix is appended by Google, not stored in the URL. It never shows in the ad’s displayed URL, so this changes live behaviour rather than just tidying files. plan shows the change; check analytics after the first clicks land.
  • An omitted attribute is unmanaged, as everywhere else. Writing custom_parameters = {} is different — that is an explicit request to clear them.

See Build tracking URLs from a shared prefix for the full worked example.

Channel and sub-type

advertising_channel_type is the broad category — search, display, video, Demand Gen. advertising_channel_sub_type is the optional refinement inside it: "VIDEO_NON_SKIPPABLE" and "VIDEO_REACH_TARGET_FREQUENCY" are two different video campaigns, and without the sub-type a .bid can’t tell them apart.

Both are fixed when the campaign is created. Nothing can change either one afterwards, in bidsmith or in the Google Ads UI — which is precisely why they’re worth writing down. If a file’s channel or sub-type doesn’t match the live campaign it adopted, plan warns: the file is describing a different campaign from the one it’s pointing at.

An omitted advertising_channel_sub_type means “not managed”, not “this campaign has none”, so adopting a campaign that has one is quiet until you declare it.

Targeting, or just watching

An audience or a demographic attached to a campaign either restricts who is eligible to see the ads or merely observes — informing bids while everyone stays eligible. targeting_setting is where a campaign says which, one dimension at a time:

resource "google_ads_campaign" "summer_search" {
name = "Summer 2026 — Search"
advertising_channel_type = "SEARCH"
campaign_budget = google_ads_campaign_budget.summer.id
targeting_setting {
target_restriction {
targeting_dimension = "AUDIENCE"
bid_only = true
}
}
}

bid_only = true is observation, false is targeting. It’s a one-word edit that can change a campaign’s reach substantially, which is exactly the kind of thing worth having in review. Declare it on the campaign or on its ad groups, never both — Google Ads refuses to write an ad group’s setting while its campaign has one, and validate warns when a file does that. The ad group page covers the same block in more detail.

Schema

Required

  • name ( String )
  • advertising_channel_type ( String ) One of: SEARCH, DISPLAY, SHOPPING, VIDEO, PERFORMANCE_MAX, MULTI_CHANNEL, LOCAL, SMART, DISCOVERY, DEMAND_GEN .
  • campaign_budget ( Reference to google_ads_campaign_budget )

Optional

  • status ( String ) One of: ENABLED, PAUSED, REMOVED . Default "ENABLED"; omit to manage at the default.
  • advertising_channel_sub_type ( String ) One of: SEARCH_MOBILE_APP, DISPLAY_MOBILE_APP, SEARCH_EXPRESS, DISPLAY_EXPRESS, SHOPPING_SMART_ADS, DISPLAY_GMAIL_AD, DISPLAY_SMART_CAMPAIGN, VIDEO_ACTION, VIDEO_NON_SKIPPABLE, APP_CAMPAIGN, APP_CAMPAIGN_FOR_ENGAGEMENT, LOCAL_CAMPAIGN, SHOPPING_COMPARISON_LISTING_ADS, SMART_CAMPAIGN, VIDEO_SEQUENCE, APP_CAMPAIGN_FOR_PRE_REGISTRATION, VIDEO_REACH_TARGET_FREQUENCY, TRAVEL_ACTIVITIES, YOUTUBE_AUDIO .
  • contains_eu_political_advertising ( String ) One of: DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING, CONTAINS_EU_POLITICAL_ADVERTISING . Default "DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING"; omit to manage at the default. Kept in the file even at its default.
  • start_date ( Date (YYYY-MM-DD) )
  • end_date ( Date (YYYY-MM-DD) )
  • languages ( List of language codes (e.g. "en") or languageConstants/NNNN )
  • locations ( List of country codes (e.g. "US") or geoTargetConstants/NNNN )
  • owns ( List of String )
  • devices ( List of String )
  • excluded_devices ( List of String )
  • callouts ( List of String )
  • final_url_suffix ( String )
  • custom_parameters ( string_map )

Nested blocks documented below: manual_cpc, manual_cpm, manual_cpv, target_cpm, target_cpv, target_impression_share, target_spend, network_settings, structured_snippet, geo_target_type_setting, video_campaign_settings, asset_automation_settings, dynamic_search_ads_setting, ai_max_setting, demand_gen_campaign_settings, targeting_setting, frequency_caps.

Bidding

Every campaign picks exactly one bidding strategy, and Google Ads refuses to create one that picks none. Which strategies are legal depends on the channel: search campaigns bid on clicks, video campaigns bid on views or impressions. Declaring two is a validation error.

BlockGoogle Ads UI nameUse it for
manual_cpcManual CPCSearch and display — you pay per click
target_impression_shareTarget impression shareSearch — Google bids to win a share of eligible impressions, under a CPC ceiling
target_spendMaximize clicksSearch — Google gets as many clicks as the budget buys, optionally under a CPC ceiling
manual_cpvMaximum CPVVideo — you pay per view, bid set on the ad group
target_cpvTarget CPVVideo — Google averages your cost per view to a target
target_cpmTarget CPMVideo reach — Google averages your cost per thousand impressions
manual_cpmMaximum CPMVideo — you pay a fixed rate per thousand impressions

The four video strategies are read-only — they record what a video campaign already bids with, because Google Ads won’t let any API client change one. See the caution below.

The video blocks carry no settings — picking the strategy is the whole declaration, and the bid amount lives on the ad group:

resource "google_ads_campaign" "preroll" {
name = "Instream Preroll — India"
status = "PAUSED"
advertising_channel_type = "VIDEO"
campaign_budget = google_ads_campaign_budget.preroll.id
manual_cpv {}
}

The search strategies do carry settings — the CPC ceiling and (for impression share) the target itself live on the campaign block, so a bid-posture change is a reviewable diff like everything else. Their attributes are documented below.

Conversion-based strategies (Target CPA, Maximize Conversions, tROAS) aren’t modelled — they need conversion tracking, which is a separate piece bidsmith doesn’t manage yet. target_impression_share and target_spend need no conversion data, which makes them the automated options for accounts without tracking.

Switching strategies on a live campaign is a normal update — swap the block and plan shows the change. refresh can’t do that swap for you; it reports the drift and leaves the block alone.

Nested blocks

manual_cpc

The manual_cpc block configures manual CPC bidding (the simplest bidding strategy — you set the max CPC, Google honors it).

Optional

  • enhanced_cpc_enabled ( Boolean )

target_impression_share

The target_impression_share block bids to win a chosen share of the auctions the campaign is eligible for, in a chosen position on the search results page. All three settings are required — this strategy has no uncapped form, and the ceiling is what actually decides what a click can cost:

resource "google_ads_campaign" "search_generic" {
name = "Search_Generic"
advertising_channel_type = "SEARCH"
campaign_budget = google_ads_campaign_budget.search_generic.id
target_impression_share {
location = "ANYWHERE_ON_PAGE"
location_fraction_micros = 800000
cpc_bid_ceiling_micros = 500000
}
}

That reads as “try to show on 80% of eligible search result pages, anywhere on the page, and never pay more than €0.50 for a click” (in a EUR account — micros are millionths of the account currency, so 800000 micros is 80% and 500000 micros is 0.50).

  • location — where on the page the share is measured: "ANYWHERE_ON_PAGE", "TOP_OF_PAGE" (above the organic results), or "ABSOLUTE_TOP_OF_PAGE" (the very first ad slot). The higher the slot, the more each impression costs.
  • location_fraction_micros — the share to aim for, as micros of 1: 800000 is 80%, 1000000 would be every eligible auction. An aggressive fraction is a spend decision worth a reviewer’s eyes — which is exactly why it lives in the file.
  • cpc_bid_ceiling_micros — the most the automated bidder may pay for one click, in micros of the account currency.

Required

  • location ( String ) One of: ANYWHERE_ON_PAGE, TOP_OF_PAGE, ABSOLUTE_TOP_OF_PAGE .
  • location_fraction_micros ( Integer )
  • cpc_bid_ceiling_micros ( Integer )

target_spend

The target_spend block is the UI’s Maximize clicks: Google buys as many clicks as the daily budget affords. The one optional setting is the CPC ceiling:

resource "google_ads_campaign" "search_ublock" {
name = "Search_uBlock"
advertising_channel_type = "SEARCH"
campaign_budget = google_ads_campaign_budget.search_ublock.id
target_spend {
cpc_bid_ceiling_micros = 1100000
}
}

Leave the attribute out (target_spend {}) and the bidder is uncapped — and because the block declares the whole strategy, an omitted ceiling is the declaration “no ceiling”: a cap added in the Google Ads UI shows up in plan as drift rather than staying invisible.

Optional

  • cpc_bid_ceiling_micros ( Integer )

network_settings

The network_settings block controls which Google networks ads can appear on — all six of them. Every field a Google Ads campaign has is here, deliberately: a block that covered some of the networks would read as a complete statement of where the money goes without being one.

An omitted field is left as-is on the account rather than turned off, so a campaign you want fully pinned down should say all six.

Optional

  • target_google_search ( Boolean )
  • target_search_network ( Boolean )
  • target_content_network ( Boolean )
  • target_partner_search_network ( Boolean )
  • target_youtube ( Boolean )
  • target_google_tv_network ( Boolean )

geo_target_type_setting

The geo_target_type_setting block decides whether the campaign’s locations mean “people there” or “people there plus people interested in there” — see In a location, or interested in it above.

Optional

  • positive_geo_target_type ( String ) One of: PRESENCE_OR_INTEREST, PRESENCE .
  • negative_geo_target_type ( String ) One of: PRESENCE_OR_INTEREST, PRESENCE .

video_campaign_settings

The video_campaign_settings block holds the settings that only apply to video campaigns. Today it carries one thing: video_ad_inventory_control.

Nested blocks documented below: video_ad_inventory_control.

video_ad_inventory_control

Which YouTube inventory the campaign’s responsive video ads may run on. This is the setting a format experiment lives or dies by: three campaigns built to test skippable in-stream are only a valid test while they stay in-stream-only, and someone switching Shorts on in the Google Ads UI would change the answer without changing the reported numbers.

resource "google_ads_campaign" "instream_us" {
name = "YouTube US — In-stream"
advertising_channel_type = "VIDEO"
campaign_budget = google_ads_campaign_budget.youtube.id
video_campaign_settings {
video_ad_inventory_control {
allow_in_stream = true
allow_in_feed = false
allow_shorts = false
allow_non_skippable_in_stream = false
}
}
}

That reads: skippable in-stream, nothing else.

Write down every inventory you care about, not just the one you want on. An omitted field is left as-is on the account rather than turned off, so a file that only says allow_in_stream = true is silent about Shorts — and silence is exactly what this block exists to end.

Optional
  • allow_in_stream ( Boolean )
  • allow_in_feed ( Boolean )
  • allow_shorts ( Boolean )
  • allow_non_skippable_in_stream ( Boolean )

asset_automation_settings

Google Ads can write ad copy for you. Left alone, it takes headlines and descriptions from your landing pages and your existing ads and adds them to the campaign’s ads — helpful on a campaign nobody is watching, and a problem on one where the wording is the point. The asset_automation_settings block is where you say no.

resource "google_ads_campaign" "brand_search" {
name = "Brand — Search"
advertising_channel_type = "SEARCH"
campaign_budget = google_ads_campaign_budget.brand.id
asset_automation_settings {
text_asset_automation = "OPTED_OUT"
final_url_expansion_text_asset_automation = "OPTED_OUT"
}
}

That reads: the ads on this campaign say what the file says, and nothing else. Turn one back on in the Google Ads UI and the next plan shows it as drift, which is what makes the opt-out stick.

bidsmith compares the automations you name, and nothing else — a campaign with no block at all is left exactly as it is. But when one of them has drifted, the whole list is written, because replacing it wholesale is the only update the Google Ads API offers here. An automation you didn’t name goes back to Google’s default at that point, so the ~ update row shows the full before-and-after:

asset_automation_settings: text_asset_automation=OPTED_IN, final_url_e… -> text_asset_automation=OPTED_OUT

Write down every automation you care about, not just the one you want off.

Optional

  • text_asset_automation ( String ) One of: OPTED_IN, OPTED_OUT .
  • final_url_expansion_text_asset_automation ( String ) One of: OPTED_IN, OPTED_OUT .
  • generate_image_enhancement ( String ) One of: OPTED_IN, OPTED_OUT .
  • generate_image_extraction ( String ) One of: OPTED_IN, OPTED_OUT .
  • generate_enhanced_youtube_videos ( String ) One of: OPTED_IN, OPTED_OUT .

Google Ads carries these settings on Search and Performance Max campaigns only; validate warns if you put the block anywhere else. Defaults differ by campaign type — on Search, text automation and final URL expansion start off; on Performance Max, everything starts on.

dynamic_search_ads_setting

Dynamic search ads are the third way Google writes your ads for you, and the broadest: instead of matching keywords you chose, it crawls your site and generates a headline and a landing page per search. use_supplied_urls_only is the line that decides how much of the site is in play.

resource "google_ads_campaign" "site_wide" {
name = "Site — dynamic"
advertising_channel_type = "SEARCH"
campaign_budget = google_ads_campaign_budget.site.id
dynamic_search_ads_setting {
domain_name = "example.com"
language_code = "en"
use_supplied_urls_only = true
}
}

Required

  • domain_name ( String )
  • language_code ( String )

Optional

  • use_supplied_urls_only ( Boolean )

domain_name and language_code are both required: a domain without the language Google should read it in is not a scope, and the API rejects half a setting.

Dynamic search ads are a Search campaign feature; validate warns if the block turns up on another channel.

ai_max_setting

AI Max is the other half of the same fence. Switched on, it lets Google match your Search campaign against queries your keywords do not cover and write ad copy for the ones it finds. That is a reasonable trade on a campaign chasing volume, and the wrong one on a campaign where the wording and the queries are the point.

resource "google_ads_campaign" "brand_search" {
name = "Brand — Search"
advertising_channel_type = "SEARCH"
campaign_budget = google_ads_campaign_budget.brand.id
ai_max_setting {
enable_ai_max = false
}
}

Optional

  • enable_ai_max ( Boolean )

Leave the block out and the campaign follows whatever Google’s default is on the day — which is the state worth knowing about, because it can change without anybody in your account touching anything. Writing the line down is what turns “probably off” into a fact the repo holds and plan checks.

Unlike asset_automation_settings above, this is an ordinary setting: one field, written on its own, nothing else in the campaign moves with it.

AI Max is a Search campaign feature; validate warns if you put the block on another channel. Its ad-group half lives on the ad group — see ai_max_ad_group_setting.

demand_gen_campaign_settings

A Demand Gen campaign’s language and location targeting lives at one of two levels — on the campaign, or on its ad groups — and Google fixes the choice when the campaign is created. Every new campaign starts in ad-group mode (“upgraded targeting”); upgraded_targeting = false is the opt-out that keeps the campaign-level languages / locations lists working:

resource "google_ads_campaign" "demand_gen_fr" {
name = "DemandGen_FR"
advertising_channel_type = "DEMAND_GEN"
campaign_budget = google_ads_campaign_budget.dg.id
target_spend {}
languages = ["fr"]
locations = ["FR"]
demand_gen_campaign_settings {
upgraded_targeting = false
}
}

Required

  • upgraded_targeting ( Boolean )

Without the opt-out, declare the targeting as language {} / location {} blocks on google_ads_ad_group_criterion resources instead — one set per ad group, which is the point of the upgraded mode: a single campaign can run a French and a German ad group side by side.

Declaring targeting at the wrong level is a validate error rather than an apply failure, because Google rejects the mismatched criteria with an error message that names no cause. The field is immutable: adopting an existing campaign means matching the level it was created with (bidsmith refresh writes the block for campaigns that opted out), and plan warns when the file says one level and the live campaign carries the other.

owns

Everything else bidsmith manages, it manages because you declared it. The assets Google’s automation attaches are the exception — you cannot write a block for one, because you did not make it and cannot remake it. owns is how a campaign claims them anyway:

resource "google_ads_campaign" "brand_search" {
name = "Brand — Search"
advertising_channel_type = "SEARCH"
campaign_budget = google_ads_campaign_budget.brand.id
owns = ["automatically_created_assets"]
}

With that line, anything Google attached to this campaign or its ad groups that your files do not declare is paused on the next apply — the business name, the logo, a sitelink invented from your landing page. Paused means it stops serving. It is not deleted, because a deleted one comes straight back: bidsmith cannot recreate what Google’s automation made, so Google simply attaches it again and every plan would propose the same removal forever. Pausing sticks.

~ pause google_ads_campaign.brand_search (paused sitelink "Also on Firefox")
(status: "ENABLED" -> "PAUSED"; attached by Google's asset automation)

You do not need the line for a kind of extension you already declare. A campaign that declares its own sitelinks owns its sitelinks, so an invented one is paused with nothing more said — the same rule that removes an undeclared sitelink someone added in the UI. owns is for the kinds no block could ever declare.

Until something claims them, plan counts them in a warning on every run instead, which is what catches someone switching the account setting back on:

warning: account: Google's asset automation is serving 6 asset(s) nothing
declares (2 BUSINESS_NAME, 2 LOGO, 2 SITELINK). Add
`owns = ["automatically_created_assets"]` to the campaign …

Account-level assets are claimed the same way, but in the provider block: they reach every campaign at once, so no single campaign can speak for them. Full walkthrough: Serve only the extensions you declared.

targeting_setting

The targeting_setting block says whether the campaign’s audiences and demographics restrict who is eligible or only inform bidding — see Targeting, or just watching above. It carries one repeated target_restriction block per dimension and no attributes of its own.

The set is managed as a whole, and only once you declare the block: a campaign with no targeting_setting leaves the setting to whoever set it in the Google Ads UI, and replacing the list wholesale is the only update the Google Ads API offers, so a dimension dropped from the file goes back to targeting. Entries that say bid_only = false are what Google assumes anyway, so bidsmith leaves them out rather than writing boilerplate into every file.

target_restriction

Required
  • targeting_dimension ( String ) One of: AUDIENCE, TOPIC, GENDER, AGE_RANGE, PLACEMENT, PARENTAL_STATUS, INCOME_RANGE .
  • bid_only ( Boolean )

KEYWORD is deliberately absent from targeting_dimension: keywords always restrict, so Google Ads has no observation mode for them.

frequency_caps

The frequency_caps block limits how often one person sees the campaign. Write one block per cap — this is the only campaign block you can repeat.

resource "google_ads_campaign" "instream" {
name = "Preroll — India"
advertising_channel_type = "VIDEO"
campaign_budget = google_ads_campaign_budget.preroll.id
frequency_caps {
event_type = "IMPRESSION"
time_unit = "DAY"
time_length = 1
cap = 3
}
frequency_caps {
event_type = "VIDEO_VIEW"
time_unit = "DAY"
time_length = 1
cap = 1
}
}

That reads as “show this campaign to the same person at most 3 times a day, and count at most 1 of those as a watched view.”

Two things worth knowing:

  • The caps are managed as a set, and only once you declare one. A campaign whose file has no frequency_caps block leaves capping to whoever set it in the Google Ads UI; write one block and the whole set becomes yours — whatever the blocks say is what the campaign ends up with, so a cap added in the UI afterwards shows up in plan as drift, and deleting every block removes every cap. Order doesn’t matter.
  • Frequency capping applies to video and display campaigns. Google Ads does not support it on Demand Gen, and validate warns if you set it there.

Required

  • event_type ( String ) One of: IMPRESSION, VIDEO_VIEW .
  • time_unit ( String ) One of: DAY, WEEK, MONTH .
  • time_length ( Integer )
  • cap ( Integer )

Optional

  • level ( String ) One of: CAMPAIGN, AD_GROUP, AD_GROUP_AD . Default "CAMPAIGN"; omit to manage at the default.

See also