Google Search Campaigns
Build Google Search campaigns with keywords, RSA ads, bidding strategies, extensions, and network settings.
Campaigns are plain TypeScript files in your campaigns/ directory. Each file exports one or more campaign objects. The CLI scans all campaigns/**/*.ts files, collects exports with a provider and kind field, and uses them as your desired state.
Basic structure
import {
google,
daily,
exact,
phrase,
broad,
headlines,
descriptions,
rsa,
url,
targeting,
geo,
languages,
negatives,
} from '@upspawn/ads'
export default google.search('Brand - Search', {
budget: daily(20),
bidding: 'maximize-clicks',
targeting: targeting(geo('US', 'CA'), languages('en')),
negatives: negatives('free', 'download', 'tutorial'),
})
.group('brand-exact', {
keywords: [...exact('my product', 'my product app')],
ad: rsa(
headlines('My Product', 'Official Site', 'Start Free Today'),
descriptions('The fastest way to get things done. Try free.'),
url('https://example.com'),
),
})
.group('brand-phrase', {
keywords: [...phrase('my product for teams', 'my product pricing')],
ad: rsa(
headlines('My Product for Teams', 'See Pricing', 'Free Trial'),
descriptions('Plans for every team size. Start free today.'),
url('https://example.com/pricing'),
),
})Budget helpers
import { daily, monthly, lifetime, eur, usd } from '@upspawn/ads'
daily(20) // $20/day
daily(eur(15)) // €15/day
monthly(500) // $500/month
lifetime(1000) // $1,000 lifetime budgetAll 9 bidding strategies
Google Search supports nine bidding strategies. The bidding field accepts a string shorthand or a full object for strategies that require extra parameters.
import { google, daily } from '@upspawn/ads'
// String shorthands (no extra parameters needed)
google.search('Campaign', { budget: daily(20), bidding: 'maximize-conversions' })
google.search('Campaign', { budget: daily(20), bidding: 'maximize-clicks' })
google.search('Campaign', { budget: daily(20), bidding: 'manual-cpc' })
google.search('Campaign', { budget: daily(20), bidding: 'manual-cpm' })
google.search('Campaign', { budget: daily(20), bidding: 'target-cpm' })
google.search('Campaign', { budget: daily(20), bidding: 'maximize-conversion-value' })
// Object form (required for strategies with parameters)
google.search('Campaign', {
budget: daily(20),
bidding: { type: 'target-cpa', targetCpa: 15 },
})
google.search('Campaign', {
budget: daily(20),
bidding: { type: 'target-roas', targetRoas: 3.0 },
})
google.search('Campaign', {
budget: daily(20),
bidding: {
type: 'target-impression-share',
location: 'absolute-top', // 'anywhere' | 'top' | 'absolute-top'
targetPercent: 80,
maxCpc: 5.00,
},
})
// Optional parameters on object form
google.search('Campaign', {
budget: daily(20),
bidding: { type: 'maximize-clicks', maxCpc: 2.50 },
})
google.search('Campaign', {
budget: daily(20),
bidding: { type: 'manual-cpc', enhancedCpc: true },
})
google.search('Campaign', {
budget: daily(20),
bidding: { type: 'maximize-conversion-value', targetRoas: 4.0 },
})Strategy summary:
| Strategy | When to use |
|---|---|
maximize-conversions | Automated — maximize conversion count within budget |
maximize-clicks | Automated — maximize clicks within budget (alias: TARGET_SPEND) |
manual-cpc | Manual per-keyword bids; add enhancedCpc: true for smart adjustments |
manual-cpm | Manual CPM bids (Display only) |
target-cpm | Automated CPM targeting (Display only) |
target-cpa | Automated — target a specific cost per conversion |
target-roas | Automated — target a return-on-ad-spend ratio |
target-impression-share | Automated — achieve a share of impression at absolute-top, top, or anywhere |
maximize-conversion-value | Automated — maximize conversion value; add targetRoas to constrain it |
Keyword match types
import { exact, phrase, broad } from '@upspawn/ads'
exact('keyword one', 'keyword two') // [keyword one], [keyword two]
phrase('keyword phrase') // "keyword phrase"
broad('keyword term') // keyword term (broad match)Mix match types in one group by spreading into an array:
keywords: [
...exact('file renamer', 'batch rename files'),
...phrase('rename files automatically'),
...broad('file organization tool'),
]RSA ads
Responsive Search Ads require 3–15 headlines (max 30 chars each) and 2–4 descriptions (max 90 chars each). Google rotates combinations to find best performers.
import { headlines, descriptions, rsa, url } from '@upspawn/ads'
rsa(
headlines(
'Rename Files Fast', // ≤ 30 chars each
'AI-Powered File Renaming',
'Batch Rename Tool',
'No Credit Card Required',
'Used by 50,000 Teams',
),
descriptions(
'Rename thousands of files in seconds. Drag, drop, done.', // ≤ 90 chars
'Free plan available. Works with photos, docs, and more.',
),
url('https://example.com/file-renamer'),
{
path1: 'rename', // shown in URL bar: example.com/rename/files
path2: 'files',
},
)RSA identity is content-based. The SDK tracks ads by a hash of their sorted headlines + descriptions + URL. Changing any headline creates a new ad and removes the old one. If you want to update ad copy without losing history, import the existing ad first.
Multiple ads per group
Put multiple RSAs in one group for A/B testing. Google will rotate them and allocate more impressions to better performers:
.group('features', {
keywords: [...exact('file renamer app')],
ad: [
rsa(
headlines('Rename Files Fast', 'AI File Renamer', 'Batch Rename Tool'),
descriptions('Drag, drop, rename. Done in seconds.', 'Free plan available.'),
url('https://example.com'),
),
rsa(
headlines('Stop Renaming Files Manually', 'AI Does It for You', 'Batch Rename'),
descriptions('Let AI rename your files automatically.', 'Try free — no card needed.'),
url('https://example.com/ai'),
),
],
})Pinned headlines
Pin a headline to a specific position to guarantee it always appears there:
rsa(
headlines('Official Site', 'AI File Renamer', 'Batch Rename Tool'),
descriptions('Try free. No credit card.', 'Rename files in seconds.'),
url('https://example.com'),
{
pinnedHeadlines: [{ text: 'Official Site', position: 1 }],
},
)Positions 1–3 correspond to the three headline slots. Pinning reduces Google's flexibility to test combinations, so use it sparingly.
Network settings
By default, Search campaigns serve on Google Search and search partners. Use networkSettings to control this:
import { google, daily, targeting, geo } from '@upspawn/ads'
google.search('Brand - Search Only', {
budget: daily(20),
bidding: 'maximize-conversions',
targeting: targeting(geo('US')),
networkSettings: {
searchNetwork: true,
searchPartners: false, // disable search partner sites
displayNetwork: false, // disable Display Network expansion
},
})| Setting | Default | What it controls |
|---|---|---|
searchNetwork | true | Serve on google.com and Google Search |
searchPartners | true | Serve on Google search partner sites (e.g. Ask.com) |
displayNetwork | false | Serve on the Display Network as a fallback |
Device bid adjustments
Increase or decrease bids for specific devices. A value of 0.2 means +20%, -1.0 means exclude entirely:
import { google, daily, targeting, geo, device } from '@upspawn/ads'
google.search('Search - Mobile Focus', {
budget: daily(30),
bidding: 'maximize-conversions',
targeting: targeting(
geo('US'),
device('mobile', 0.3), // +30% on mobile
device('desktop', 0.0), // no adjustment on desktop
device('tablet', -0.5), // -50% on tablet
),
})To exclude a device entirely, set its bid adjustment to -1.0.
Extensions
Extensions appear below your ad text. Add them with chained methods after .group().
Sitelinks
import { google, daily, link } from '@upspawn/ads'
google.search('Search Campaign', { budget: daily(20), bidding: 'maximize-clicks' })
.group('keywords', { /* ... */ })
.sitelinks(
link('Pricing', 'https://example.com/pricing', {
description1: 'Plans for every team size',
description2: 'Free plan available',
}),
link('Features', 'https://example.com/features'),
link('Blog', 'https://example.com/blog'),
link('Sign Up', 'https://example.com/signup'),
)Callouts
Short phrases (max 25 chars each) that highlight your benefits:
.callouts('Free Trial', 'No Credit Card', 'Cancel Anytime', '24/7 Support')Structured snippets
import { google, daily } from '@upspawn/ads'
google.search('Campaign', { budget: daily(20), bidding: 'maximize-clicks' })
.group('keywords', { /* ... */ })
.snippets(
{ header: 'Services', values: ['File Renaming', 'Batch Processing', 'AI Rules'] },
)Campaign-level negatives
Negatives prevent your ads from showing on irrelevant searches:
import { google, daily, targeting, geo, negatives, broad, phrase } from '@upspawn/ads'
google.search('File Renamer - Search', {
budget: daily(20),
bidding: 'maximize-conversions',
targeting: targeting(geo('US')),
negatives: [
...broad('free', 'open source', 'freeware'),
...phrase('how to rename files manually'),
],
})Ad-group-level negatives are also supported:
.group('premium-features', {
keywords: [...exact('file renamer pro')],
negatives: [...exact('free file renamer')],
ad: rsa(/* ... */),
})Multi-locale campaigns
Use .locale() to add ad groups with targeting overrides — ideal for multilingual campaigns where each locale needs different geo and language targeting:
import {
google, daily, phrase, headlines, descriptions, rsa, url,
targeting, geo, languages,
} from '@upspawn/ads'
export default google.search('File Renamer - Global', {
budget: daily(40),
bidding: 'maximize-conversions',
})
.locale('en-us', targeting(geo('US', 'CA'), languages('en')), {
keywords: [...phrase('file renamer', 'rename files')],
ad: rsa(
headlines('Rename Files Fast', 'AI File Renamer', 'Batch Rename Tool'),
descriptions('Rename thousands of files in seconds.', 'Free plan available.'),
url('https://example.com'),
),
})
.locale('de', targeting(geo('DE', 'AT', 'CH'), languages('de')), {
keywords: [...phrase('dateien umbenennen', 'datei umbenenner')],
ad: rsa(
headlines('Dateien umbenennen', 'KI-gestützt', 'Stapelumbenennung'),
descriptions('Tausende Dateien in Sekunden umbenennen.', 'Kostenlos testen.'),
url('https://example.com/de'),
),
})Or export multiple campaign objects from one file:
import { google, daily, phrase, headlines, descriptions, rsa, url } from '@upspawn/ads'
const base = { budget: daily(20), bidding: 'maximize-clicks' as const }
export const campaignEn = google.search('Brand - EN', base)
.group('brand-en', {
keywords: [...phrase('my product')],
ad: rsa(
headlines('My Product', 'Official Site', 'Start Free'),
descriptions('The fastest way to get things done.', 'No card required.'),
url('https://example.com'),
),
})
export const campaignDe = google.search('Brand - DE', base)
.group('brand-de', {
keywords: [...phrase('mein produkt')],
ad: rsa(
headlines('Mein Produkt', 'Offizielle Seite', 'Kostenlos starten'),
descriptions('Der schnellste Weg, Dinge zu erledigen.', 'Keine Karte nötig.'),
url('https://example.com/de'),
),
})Campaign discovery
The CLI automatically discovers campaigns by scanning campaigns/**/*.ts and looking for exports with provider and kind fields. Both default exports and named exports work:
// Default export
export default google.search('Campaign', { /* ... */ })
// Named exports — both are discovered
export const campaignEn = google.search('Campaign EN', { /* ... */ })
export const campaignDe = google.search('Campaign DE', { /* ... */ })Run ads validate to check all campaign files for errors without hitting the API.
Resource paths
Every resource gets a stable path used as its identifier:
campaign-name/ad-group-name/kw:keyword-text:EXACT
campaign-name/ad-group-name/rsa:a3f2b1c4...These paths map to platform IDs in the SQLite cache. Renaming a campaign in code is treated as a delete + create. If you want to rename without recreating, import first and update the cache mapping.
See also
- Display campaigns — image-based ads across the web
- Performance Max — AI-assembled ads across all Google channels
- Shopping campaigns — product ads from Merchant Center
- Shared config — reuse targeting and budget across campaigns