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"]}locationstakes ISO 3166-1 alpha-2 country codes ("US","GB","SG", …). For a city, region, or anything finer-grained, drop in the rawgeoTargetConstants/NNNNstring in the same list — look the id up with Google’s geo target constant table.languagestakes short language codes ("en","de","pl","zh-CN", …). Less common languages can be given as a rawlanguageConstants/NNNNstring.
Two rules keep targeting unambiguous:
- A campaign declares each axis once. Setting
locationson the campaign and a positivegoogle_ads_campaign_criterionwith alocation {}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, andTABLETare implied by omission.CONNECTED_TV(video campaigns) andOTHERare never switched off behind your back — name them indevicesto target them. - A bid adjustment is not an exclusion.
bid_modifier = 0.8on mobile means “bid less here”, which no spelling of these attributes can express; keep it as an explicitgoogle_ads_campaign_criterion.refreshleaves such campaigns in the explicit form. - As with
locationsandlanguages, a campaign declares the axis once. Settingdevicesand adevice {}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.
planshows 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 togoogle_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.
| Block | Google Ads UI name | Use it for |
|---|---|---|
manual_cpc | Manual CPC | Search and display — you pay per click |
target_impression_share | Target impression share | Search — Google bids to win a share of eligible impressions, under a CPC ceiling |
target_spend | Maximize clicks | Search — Google gets as many clicks as the budget buys, optionally under a CPC ceiling |
manual_cpv | Maximum CPV | Video — you pay per view, bid set on the ad group |
target_cpv | Target CPV | Video — Google averages your cost per view to a target |
target_cpm | Target CPM | Video reach — Google averages your cost per thousand impressions |
manual_cpm | Maximum CPM | Video — 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:800000is 80%,1000000would 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_OUTWrite 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) nothingdeclares (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_capsblock 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 inplanas 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
validatewarns 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
google_ads_campaign_budget— required reference.google_ads_ad_group— child resources.google_ads_campaign_criterion— campaign-level negatives, geo targets, language targets.- Tutorial: Launch a new search campaign.