@koda-sl/baker-cli
v0.115.0
Published
AI-agent-first CLI for interacting with Baker, including the Baker creative canvas.
Readme
@koda-sl/baker-cli
AI-agent-first CLI for interacting with Baker. Designed for programmatic use by AI agents with structured JSON output, schema introspection, self-correcting errors, and built-in caching.
Installation
baker --version reports the installed package version from @koda-sl/baker-cli/package.json.
npm install @koda-sl/baker-cli
# or
pnpm add @koda-sl/baker-cliAuthentication
Set environment variables:
export BAKER_API_KEY="bk_your_api_key_here"
export BAKER_API_URL="https://your-baker-instance.convex.site"
# Optional: default customer ID for Google Ads commands
export BAKER_GOOGLE_ADS_CUSTOMER_ID="1234567890"
# Optional: default GA4 property ID
export BAKER_GA4_PROPERTY_ID="properties/123456789"
# Optional: default GSC site URL
export BAKER_GSC_SITE_URL="https://example.com/"
# Optional: default X Ads account ID (base36)
export BAKER_X_ADS_ACCOUNT_ID="18ce53xyz"
# Required for `baker actions ...` and `baker scheduled-actions create/update/delete` commands that stage against a chat
export BAKER_CHAT_ID="<chat-id>"BAKER_API_KEYmust start withbk_BAKER_API_URLis your Convex site URLBAKER_GOOGLE_ADS_CUSTOMER_ID— default Google Ads customer ID (10 digits). Used when--customer-idis not passed. If neither is set and the account has exactly one Google Ads customer, it is auto-selected.BAKER_GA4_PROPERTY_ID— default GA4 property ID. Used when--property-idis not passed. If neither is set and exactly one property is connected, it is auto-selected.BAKER_GSC_SITE_URL— default GSC site URL. Used when--site-urlis not passed. If neither is set and exactly one site is verified, it is auto-selected.BAKER_X_ADS_ACCOUNT_ID— default X Ads account ID (base36). Used when--account-idis not passed. If neither is set and exactly one X Ads account is connected, it is auto-selected.BAKER_CHAT_ID— chat context for staged Work Action commands and staged Scheduled Action create/update/delete.scheduled-actions triggerdoes not require it.
Output Format
All commands return a JSON envelope:
// Success
{ "ok": true, "data": { ... } }
// Success with field descriptions
{ "ok": true, "data": [...], "fields": { "campaign.name": "Campaign display name", "metrics.clicks": "Total clicks" } }
// Success with data disclaimer (research commands)
{ "ok": true, "data": [...], "fields": { ... }, "note": "Estimates based on third-party SERP data — not exact figures. Use for directional insights, not precise measurement." }
// Success with geo targeting context (keyword/research commands)
{ "ok": true, "data": [...], "query_context": { "location": "United States (2840)", "language": "English (1000)", "location_is_default": true, "language_is_default": true, "defaults_warning": "Location and/or language were not specified — using defaults. Results are for United States in English. Pass --location and --language to target a different market." } }
// Error with self-correction (ads commands)
{ "ok": false, "error": { "code": "FIELD_NOT_FOUND", "message": "...", "fix": { "action": "retry_with_modified_query", "correctedCommand": "baker ads google query \"...\"", "explanation": "..." }, "retryable": false, "gaqlExecuted": "SELECT ..." } }
// Dry-run
{ "ok": true, "dryRun": true, "operation": "images.delete", "params": { "id": "abc123" } }Use --output to change format:
| Format | Description | Best for |
|---------|------------------------------------|--------------------|
| json | Structured JSON envelope (default) | AI agents |
| csv | RFC 4180 comma-separated values | Analysis tools |
| jsonl | One JSON object per line | Streaming/appending |
| files | Tab-separated, one row per result | Piping / shell |
| md | Markdown table | Human reading |
Commands
Ad Platforms (baker ads)
Multi-platform ad data commands. Currently supports Google Ads, with Meta, LinkedIn, and TikTok coming.
baker ads google accounts
List all accessible Google Ads accounts. Run this first to find account IDs.
baker ads google accountsResponse:
{
"ok": true,
"data": [
{ "id": "3343516765", "name": "My Company", "access_type": "direct", "level": 0 },
{ "id": "4106495409", "name": "Client Account", "access_type": "managed", "manager_id": "2292659431", "level": 1 }
]
}Flags:
| Flag | Description |
|--------------|-----------------------|
| --no-cache | Skip cache (1h TTL) |
baker ads google currency
Get the account currency. Call before interpreting cost_micros values.
baker ads google currency --customer-id 5904042878Response:
{
"ok": true,
"data": { "currency_code": "EUR", "customer_id": "5904042878" }
}Flags:
| Flag | Description |
|-----------------|------------------------------------------------|
| --customer-id | Google Ads customer ID (10 digits, no dashes). Falls back to BAKER_GOOGLE_ADS_CUSTOMER_ID env var. |
| --no-cache | Skip cache (24h TTL) |
baker ads google query
Run arbitrary GAQL queries. The most powerful command.
# Raw GAQL
baker ads google query "SELECT campaign.name, metrics.clicks, metrics.cost_micros FROM campaign WHERE segments.date DURING LAST_7_DAYS AND campaign.status = 'ENABLED' ORDER BY metrics.cost_micros DESC" --customer-id 1234567890
# Use a preset (saves tokens)
baker ads google query --preset campaign-performance --customer-id 1234567890
# Export to CSV
baker ads google query --preset search-terms --customer-id 1234567890 --out results.csv
# Auto-paginate large datasets
baker ads google query "SELECT ..." --customer-id 1234567890 --all --out /tmp/export.csv
# List available presets
baker ads google query --list-presetsResponse (JSON):
{
"ok": true,
"data": [
{ "campaign.name": "Brand US", "metrics.clicks": 4521, "metrics.cost_micros": 2850000000 }
],
"fields": {
"campaign.name": "Campaign display name",
"metrics.clicks": "Total clicks (integer)",
"metrics.cost_micros": "Total cost in micros (÷ 1,000,000 for actual currency)"
}
}Response (with pagination):
{
"ok": true,
"data": [...],
"fields": { ... },
"pagination": { "hasMore": true, "cursor": "eyJwYWdl..." }
}Response (file output, stdout only):
{
"ok": true,
"fields": { ... },
"file": "/tmp/results.csv",
"rows": 12847
}Flags:
| Flag | Description |
|-----------------|--------------------------------------------------------------|
| --customer-id | Google Ads customer ID (10 digits, no dashes). Falls back to BAKER_GOOGLE_ADS_CUSTOMER_ID env var. |
| --preset | Named query template (see presets below) |
| --date-range | Override preset date range (LAST_7_DAYS, LAST_30_DAYS, etc.) |
| --limit | Max rows per page (default 200) |
| --cursor | Pagination cursor from previous response |
| --all | Auto-paginate all results |
| --out | Write data to file (format from extension: .csv, .jsonl, .json) |
| --append | Append to existing file (skip CSV headers) |
| --output | Output format: json | csv | jsonl | md |
| --no-cache | Skip cache |
| --list-presets| List all available presets |
Presets:
| Preset | Description | Default date range |
|------------------------|------------------------------------------|--------------------|
| campaign-performance | Campaign metrics overview | LAST_30_DAYS |
| keyword-analysis | Keyword performance with match type | LAST_30_DAYS |
| positive-keywords | Positive (targeting) keywords only | ALL_TIME |
| negative-keywords | Negative (blocking) keywords only | ALL_TIME |
| search-terms | Actual user queries triggering ads | LAST_7_DAYS |
| ad-copy-performance | Ad headline/description effectiveness | LAST_30_DAYS |
| asset-performance | PMax asset performance labels | LAST_30_DAYS |
| shopping-products | Product-level shopping metrics | LAST_30_DAYS |
| account-summary | Account-level totals | LAST_30_DAYS |
Pre-flight checks and auto-fixes:
The CLI rejects or automatically corrects common GAQL mistakes before hitting the API:
| Pattern | Behavior | Warning/error emitted |
|---------|----------|-----------------------|
| Missing LIMIT | Adds LIMIT 200 | "Added LIMIT 200. Use --limit to override." |
| Bare keyword.text / keyword.match_type | Rewrites to ad_group_criterion.* (resource-scoped forms like shared_criterion.keyword.text pass through untouched) | "keyword.text → ad_group_criterion.keyword.text" |
| shopping_performance_view.product_* | Rewrites to segments.product_* | Field renamed |
| campaign.status = 'ACTIVE' | Rewrites to = 'ENABLED' | Enum corrected |
| segments.conversion_action_name with metrics.cost_micros | Rejects; split into separate conversion-action and spend queries | INCOMPATIBLE_FIELDS with fix.action: "split_query" |
| FROM ad_group_criterion without negative field | Warns | "ad_group_criterion returns BOTH positive and negative keywords. Use --preset positive-keywords or negative-keywords." |
Auto-fixed queries proceed normally. Warnings appear in the response:
{ "ok": true, "data": [...], "warnings": [{ "code": "FIELD_RENAMED", "message": "keyword.text → ad_group_criterion.keyword.text" }] }Self-correcting errors:
When a query fails, the error includes a fix object with the exact corrected command:
{
"ok": false,
"error": {
"code": "INVALID_OPERATOR",
"message": "GAQL does not support CONTAINS. Use LIKE with % wildcards.",
"fix": {
"action": "change_operator",
"correctedCommand": "baker ads google query \"SELECT campaign.name FROM campaign WHERE campaign.name LIKE '%brand%' LIMIT 200\" --customer-id 1234567890",
"explanation": "GAQL uses LIKE '%value%' for substring matching, not CONTAINS()"
},
"retryable": false
}
}baker ads google changes
Get recent change logs for an account.
baker ads google changes --customer-id 1234567890
baker ads google changes --customer-id 1234567890 --days 14 --resource-type CAMPAIGNFlags:
| Flag | Description |
|-------------------|---------------------------------------------------------------|
| --customer-id | Google Ads customer ID. Falls back to BAKER_GOOGLE_ADS_CUSTOMER_ID env var. |
| --days | Lookback days (default 7, max 90) |
| --resource-type | Filter: CAMPAIGN, AD_GROUP, AD_GROUP_AD, AD_GROUP_CRITERION |
| --limit | Max results (default 50) |
| --output | Format: json | csv | jsonl | md |
baker ads google keywords discover
Discover keyword ideas from seed keywords or URLs.
baker ads google keywords discover --customer-id 1234567890 --seeds "running shoes,athletic footwear"
baker ads google keywords discover --customer-id 1234567890 --url "https://competitor.com" --limit 50Response:
{
"ok": true,
"data": {
"keywords": [
{
"keyword": "student services",
"avg_monthly_searches": "1000",
"competition": "LOW",
"competition_index": "4",
"low_top_of_page_bid_micros": 0,
"high_top_of_page_bid_micros": 0,
"monthly_search_volumes": [
{ "year": "2025", "month": "APRIL", "monthly_searches": "590" }
]
}
],
"total_results": 10,
"next_page_token": "..."
},
"fields": {
"keyword": "Suggested keyword text",
"avg_monthly_searches": "Average monthly search volume",
"competition": "Competition level: LOW, MEDIUM, HIGH, UNKNOWN",
"competition_index": "Competition index 0-100 (higher = more competitive)",
"low_top_of_page_bid_micros": "Low-range CPC bid in micros (÷ 1,000,000 for currency)",
"high_top_of_page_bid_micros": "High-range CPC bid in micros (÷ 1,000,000 for currency)"
}
}Flags:
| Flag | Description |
|-----------------|-----------------------------------------------|
| --customer-id | Google Ads customer ID. Falls back to BAKER_GOOGLE_ADS_CUSTOMER_ID env var. |
| --seeds | Comma-separated seed keywords (max 10) |
| --url | URL to extract keyword ideas from |
| --location | Geo target ID. Defaults to 2840 (United States) — always set for non-US markets |
| --language | Language ID. Defaults to 1000 (English) — always set for non-English markets |
| --limit | Max results (default 20) |
| --cursor | Pagination cursor from previous response (next_page_token) |
| --no-cache | Skip cache (24h TTL) |
| --output | Format: json | csv | jsonl | md |
Note: The response includes a
query_contextobject showing the actual location and language used, withdefaults_warningwhen defaults were applied.
baker ads google keywords metrics
Get historical metrics for specific keywords.
baker ads google keywords metrics --customer-id 1234567890 --keywords "running shoes,nike shoes,adidas shoes"Response:
{
"ok": true,
"data": {
"historical_metrics": [
{
"keyword": "running shoes",
"avg_monthly_searches": "1000",
"competition": "LOW",
"competition_index": "4",
"low_top_of_page_bid_micros": 0,
"high_top_of_page_bid_micros": 0,
"monthly_search_volumes": [
{ "year": "2025", "month": "APRIL", "monthly_searches": "590" }
]
}
]
},
"fields": { ... },
"query_context": { "location": "United States (2840)", "language": "English (1000)", "location_is_default": true, "language_is_default": true }
}Flags:
| Flag | Description |
|-----------------|-----------------------------------------------|
| --customer-id | Google Ads customer ID. Falls back to BAKER_GOOGLE_ADS_CUSTOMER_ID env var. |
| --keywords | Comma-separated keywords to analyze (max 200) |
| --location | Geo target ID. Defaults to 2840 (United States) — always set for non-US markets |
| --language | Language ID. Defaults to 1000 (English) — always set for non-English markets |
| --no-cache | Skip cache (24h TTL) |
| --output | Format: json | csv | jsonl | md |
Google Ads Library (baker ads google library)
Manage and search the Google Ads Transparency Center. Track competitor advertisers, browse their ad creatives, and discover who's bidding on keywords.
Typical workflow: search-advertiser → track → search-ads
baker ads google library search-advertiser "query"
Search for an advertiser on the Google Ads Transparency Center.
Recommended: use the domain running the ads (e.g.
example.com) for more accurate results.
baker ads google library search-advertiser "example.com"
baker ads google library search-advertiser "Nike"Response:
{
"ok": true,
"data": {
"results": [
{ "advertiserId": "AR12345678901234567", "name": "Nike, Inc.", "region": "US", "format": "TEXT_IMAGE_VIDEO" }
]
}
}Flags:
| Flag | Description |
|------------|--------------------------------|
| --output | Format: json | csv | md |
baker ads google library track <id> <name>
Track a new Google advertiser and wait for the initial ad sync to complete. Polls every 5 seconds with a 10-minute timeout. Progress is written to stderr.
baker ads google library track AR12345678901234567 "Nike, Inc."
baker ads google library track AR12345678901234567 "Nike, Inc." --jsonResponse (with --json):
{
"ok": true,
"data": {
"advertiserId": "ar_abc123",
"accountId": "acc_def456",
"totalAdCount": 342,
"activeAdCount": 89
}
}Flags:
| Flag | Description |
|----------|----------------------|
| --json | Output in JSON format |
baker ads google library list-advertisers
List all tracked Google advertisers and their accounts.
baker ads google library list-advertisers
baker ads google library list-advertisers --output mdFlags:
| Flag | Description |
|------------|--------------------------------|
| --output | Format: json | csv | md |
baker ads google library sync-status <accountId>
Check the sync status and ad counts of a tracked account.
baker ads google library sync-status acc_def456Response:
{
"ok": true,
"data": {
"syncStatus": null,
"totalAdCount": 342,
"activeAdCount": 89
}
}syncStatus is null when idle, "syncing" during a sync, or "error" if the last sync failed.
baker ads google library search-ads <accountId>
Search and filter ads for a tracked account. Supports pagination.
baker ads google library search-ads acc_def456
baker ads google library search-ads acc_def456 --search "summer sale" --isActive --mediaType image
baker ads google library search-ads acc_def456 --sort newest --limit 50
baker ads google library search-ads acc_def456 --cursor "eyJwYWdl..."Response:
{
"ok": true,
"data": {
"page": [
{
"_id": "abc123",
"platform": "google",
"externalId": "CR_1234567890",
"isActive": true,
"mediaType": "image",
"headline": "Summer Sale — 50% Off Everything",
"description": "Shop our biggest sale of the year. Free shipping on all orders.",
"destinationUrl": "https://example.com/summer-sale",
"bodyText": "Summer Sale — 50% Off Everything",
"pageName": "Example Store",
"impressionsMin": 100000,
"impressionsMax": 200000,
"startDate": "2025-06-01",
"endDate": "2025-06-30",
"firstSeenAt": 1717200000000,
"lastSeenAt": 1719792000000,
"publisherPlatforms": ["GOOGLE_ADS"],
"regionCodes": ["US", "GB"],
"variations": [
{
"headline": "Summer Sale — 50% Off",
"description": "Shop our biggest sale of the year.",
"destinationUrl": "https://example.com/summer-sale",
"imageUrl": "https://...",
"visibleUrl": "example.com"
}
],
"regions": [
{ "code": "US", "name": "United States" }
],
"analysisStatus": "completed",
"aiAnalysis": {
"aiSummary": "Promotional display ad for a seasonal sale with urgency-driven CTA",
"hookAngle": "Discount/Price",
"offerType": "Percentage Discount",
"ctaStrategy": "Shop Now",
"funnelStage": "Bottom",
"targetAudience": "Price-sensitive shoppers",
"adFormat": "responsive_display",
"tags": ["sale", "discount", "ecommerce"],
"trustSignals": ["Free shipping"],
"keyMessages": ["50% off", "Free shipping"],
"competitiveAngle": "Price leadership",
"dominantColors": ["#FF5733", "#FFFFFF"],
"analyzedAt": 1719792000000
}
}
],
"continueCursor": "eyJwYWdl...",
"isDone": false
}
}Key response fields:
| Field | Description |
|-------|-------------|
| headline, description | Top-level ad copy (first variation) |
| variations[] | All ad variations with copy, images, videos, and URLs |
| regions[] | Geographic targeting regions |
| impressionsMin/Max | Estimated impression range (Google Ads Transparency data) |
| publisherPlatforms | Where the ad ran (GOOGLE_ADS, YOUTUBE, etc.) |
| analysisStatus | AI analysis state: pending, processing, completed, failed |
| aiAnalysis | AI-generated creative analysis (only present when analysisStatus is completed) |
| aiAnalysis.aiSummary | One-line AI summary of the ad |
| aiAnalysis.hookAngle | Creative hook (Discount, Fear, Social Proof, etc.) |
| aiAnalysis.funnelStage | Funnel position: Top, Middle, Bottom |
| aiAnalysis.tags | AI-generated tags for filtering |
Flags:
| Flag | Description |
|---------------|------------------------------------------------|
| --search | Search term for ad text |
| --isActive | Filter by active ads only |
| --mediaType | Filter by media type: image, video, text |
| --sort | Sort: newest or oldest |
| --limit | Max results per page (default 20, max 100) |
| --cursor | Pagination cursor from previous response |
| --output | Format: json | csv | md |
baker ads google library sync <accountId>
Trigger an immediate re-sync for a tracked account. Polls every 5 seconds until complete (10-minute timeout). Progress is written to stderr.
baker ads google library sync acc_def456Response:
{
"ok": true,
"data": {
"totalAdCount": 350,
"activeAdCount": 92
}
}baker ads google library search-competitors "keyword"
Search for competitors running Google ads for a keyword. Uses DataForSEO (same data as baker research advertisers).
baker ads google library search-competitors "running shoes"
baker ads google library search-competitors "crm software" --location ukFlags:
| Flag | Description |
|--------------|----------------------------|
| --location | Location name or code |
| --json | Output in JSON format |
Staged writes (baker ads google budgets|campaigns|...)
Write commands never touch the Google Ads API at stage time. Each command stages a create/update/pause/resume/remove op against the current chat's draft (BAKER_CHAT_ID); the dashboard shows it as a pending "Google Ads" change, and the whole draft applies as one atomic GoogleAdsService.Mutate when the chat is published. Feature-flagged per company (companies.googleAdsWriteEnabled) — off by default = a fully simulated publish with zero real API calls.
Reference not-yet-created resources with g_temp_* refs returned by earlier create commands (they map to Google's temporary negative-ID resource names inside one atomic batch).
# Build a Search campaign bottom-up, chaining temp refs
baker ads google budgets create --customer-id 1234567890 --name "Search budget" --amount 50
# → { ref: "g_temp_ab12", ... }
baker ads google campaigns create --customer-id 1234567890 --name "Brand — Search" \
--channel-type SEARCH --budget-ref g_temp_ab12 --bidding-strategy MANUAL_CPC
baker ads google ad-groups create --customer-id 1234567890 --name "Brand terms" --campaign-ref g_temp_<campaign>
baker ads google keywords add --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --text "brand name" --match-type EXACT
# Shared negative keyword list → attach to the campaign
baker ads google keyword-lists create --customer-id 1234567890 --name "Brand exclusions"
baker ads google keyword-lists add --customer-id 1234567890 --list-ref g_temp_<list> --text "free" --match-type BROAD
baker ads google keyword-lists attach --customer-id 1234567890 --campaign-ref g_temp_<campaign> --list-ref g_temp_<list>
# Batch keywords — one command stages the whole set as ONE request (up to 500)
baker ads google keywords add --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> \
--text "brand name, brand shop:PHRASE, brand store" --match-type EXACT
baker ads google keyword-lists add --customer-id 1234567890 --list-ref <list-id> --file negatives.txt
# negatives.txt: one keyword[:MATCH_TYPE] per line; blank lines and "# comments" are skipped
# Responsive search ad
baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> \
--headlines "Fast Widgets,Buy Online,Free Shipping" --descriptions "Best widgets around.,Ships tomorrow." \
--final-url https://example.com
# Responsive display ad — images are fields on the ad; register each image as an asset first (returns a g_temp_* ref)
baker ads google assets create --customer-id 1234567890 --file '{"type":"image","imageId":"<baker_image_id>","name":"Hero"}'
baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --format responsiveDisplay \
--headlines "Come back and save" --long-headline "Finish signing up and get 20% off" \
--descriptions "Fast setup, no card required" --business-name "Acme" --final-url https://example.com \
--marketing-images g_temp_<img_landscape> --square-marketing-images g_temp_<img_square> --logo-images g_temp_<img_logo>
# Review / undo staged changes
baker ads google draft list # readable campaign ▸ ad group ▸ ad tree + completeness advisories
baker ads google draft list --json # raw JSON envelope for scripting
baker ads google draft remove g_temp_ab12 # cascades to dependents
baker ads google draft clearCommand groups: budgets, campaigns, ad-groups, keywords (add/update/remove), negative-keywords, keyword-lists, ads, assets, audiences, conversions, bidding-strategies, labels, campaign-criteria, and draft. Amounts are in major currency units (converted to micros). Money/bids: --amount, --cpc-bid, --target-cpa take major units; --target-roas a ratio. Less-common ops accept a --file <payload.json> (flags override file keys). Updates target a resource name or bare id as the positional argument; a target that names an op staged earlier amends it in place.
Reviewing the draft — baker ads google draft list renders everything you've staged as a grouped campaign ▸ ad group ▸ ad tree (with the simulated/live mode banner and non-blocking completeness advisories), the CLI counterpart to the dashboard's Google Ads tab. Pass --json for the raw envelope. Aim for a fully built campaign — 2–4 ad groups, ≥5 keywords each, 2–4 RSAs with 8–12 headlines, ≥4 sitelinks / ≥3 callouts / ≥1 structured snippet, and ≥1 shared negative list; the advisories flag what's still thin.
Batch keyword adds — keywords add, negative-keywords add, and keyword-lists add take a whole batch in one command: comma-separate --text entries and/or pass --file <list.txt> (one keyword per line). A :EXACT/:PHRASE/:BROAD suffix per entry overrides the --match-type default. Batches stage all-or-nothing as one request (limit 500); each keyword still lands as its own draft op, so it stays individually removable/amendable.
Caching
Google Ads data is cached at two levels:
Server-side (ActionCache) — shared across all CLI instances and agents for the same company:
| Data type | TTL | Cache name |
|-----------|-----|------------|
| Account list | 1 hour | ads-accounts-v1 |
| Currency | 7 days | ads-currency-v1 |
| GAQL queries | 1 hour | ads-query-v1 |
| Keyword ideas | 1 day | ads-keyword-ideas-v1 |
| Keyword metrics | 1 day | ads-keyword-metrics-v1 |
Client-side (local file cache) — per-machine at ~/.baker/cache/ads/:
| Data type | TTL | Reason | |-----------|-----|--------| | GAQL (LAST_30_DAYS, BETWEEN) | 6 hours | Historical data is immutable | | GAQL (LAST_7_DAYS) | 1 hour | Recent data updates less frequently | | GAQL (TODAY) | 15 minutes | Today's data changes in real-time |
Use --no-cache on any command to bypass both the local file cache and the server-side ActionCache, forcing a fresh API call.
Google Ads Error Codes
| Code | Retryable | Meaning |
|------|-----------|---------|
| FIELD_NOT_FOUND | No | Invalid field in GAQL query |
| WRONG_RESOURCE | No | Field queried from wrong resource |
| INVALID_OPERATOR | No | Unsupported operator (e.g. CONTAINS) |
| CUSTOMER_NOT_FOUND | No | Invalid customer ID — run baker ads google accounts to find valid IDs |
| MISSING_MANAGER_ID | No | Account not accessible (may need MCC login-customer-id) |
| INCOMPATIBLE_FIELDS | No | Fields can't be in same query |
| OPEN_ENDED_DATE | No | Date range must be finite |
| READ_ONLY | No | Only SELECT queries allowed |
| QUOTA_EXCEEDED | Yes (30s) | API rate limit hit |
| TIMEOUT | Yes (5s) | Query too broad |
| AUTH_ERROR | No | Token expired or missing |
All errors include a fix object with action, correctedCommand (when applicable), and explanation.
X (Twitter) Ads (baker ads x)
Read X Ads campaigns, line items, promoted tweets, creatives, audiences, and analytics. Powered by the X Ads API v12.
Environment:
BAKER_X_ADS_ACCOUNT_ID— default account ID (base36). Used when--account-idis not passed. If neither is set and exactly one X Ads account is connected, it's auto-selected.
Subcommands:
| Subcommand | What it returns |
|---|---|
| accounts | All accessible X Ads accounts |
| funding | Funding instruments for an account |
| campaigns | Campaigns (filter by --funding-instrument-ids, --campaign-ids) |
| line-items | Line items / ad groups (filter by --campaign-ids, --line-item-ids) |
| promoted-tweets | Promoted tweets (filter by --line-item-ids) |
| cards | Website cards, video cards, carousels |
| media | Media library (images / GIFs / videos) — --media-type IMAGE\|GIF\|VIDEO |
| audiences | Custom audiences (size, targetable status) |
| targeting-criteria | Targeting attached to line items |
| targeting-constants | Lookup locations / interests / events / devices etc. — --constant <name> --q "Madrid" |
| active-entities | Entities with metric activity in a time range |
| stats sync | Synchronous analytics (≤7 days, no segmentation) |
| stats job | Async stats end-to-end (creates + polls + downloads + decompresses). Must run in the background (the harness enforces this). Use for ranges >7 days, segmented stats, or when sync limits are hit. |
| stats job-create | Low-level: create async stats job, return ID immediately |
| stats job-status | Low-level: poll job status / get download URL |
Examples:
baker ads x accounts
baker ads x campaigns --account-id 18ce53xyz
baker ads x line-items --account-id 18ce53xyz --campaign-ids abc
baker ads x promoted-tweets --account-id 18ce53xyz
# Sync analytics with a preset (saves tokens)
baker ads x stats sync --preset campaign-engagement-7d --entity-ids abc,def
# Free-form sync stats
baker ads x stats sync --account-id 18ce53xyz --entity LINE_ITEM \
--entity-ids abc,def --start-time 2026-05-01T00:00:00Z --end-time 2026-05-07T00:00:00Z \
--metric-groups ENGAGEMENT,BILLING --granularity DAY
# Async job, sync from the CLI's perspective (creates → polls → downloads → returns).
# Must be invoked with run_in_background: true. The harness will block otherwise.
baker ads x stats job --account-id 18ce53xyz --entity CAMPAIGN \
--entity-ids abc --start-time 2026-04-01T00:00:00Z --end-time 2026-05-01T00:00:00Z \
--metric-groups ENGAGEMENT,BILLING --segmentation-type LOCATIONS
# Low-level (don't wait, manage polling yourself):
baker ads x stats job-create --account-id 18ce53xyz --entity CAMPAIGN \
--entity-ids abc --start-time 2026-04-01T00:00:00Z --end-time 2026-05-01T00:00:00Z \
--metric-groups ENGAGEMENT,BILLING --segmentation-type LOCATIONS
baker ads x stats job-status --account-id 18ce53xyz --job-id <jobId>
# List sync presets
baker ads x stats sync --list-presets
# Targeting lookups
baker ads x targeting-constants --constant locations --q "Madrid" --country-code ES
baker ads x targeting-constants --constant interestsCaching: account list 1h · campaigns/line items/promoted tweets 1h (date-keyed) · cards/media/audiences 6h · stats sync 1h · targeting constants 7 days. Pass --no-cache to bypass.
Rate limits: server-side buckets (xAds:read, xAds:write, xAds:analyticsSync, xAds:analyticsAsync, xAds:audiences) sit well under X's published quotas; 429s honor x-account-rate-limit-reset / x-rate-limit-reset headers.
Meta Ads (baker ads meta)
Meta Marketing API (Facebook + Instagram). Connect via OAuth in the dashboard, pick which ad accounts to scope to, then call from the CLI. Account ID via --account-id act_123 or BAKER_META_AD_ACCOUNT_ID env var.
The command surface is curated for AI agents, not 1:1 with the Marketing API. Smart defaults so an agent doesn't need to remember every flag.
Common AI questions, mapped to commands
# "How is account X doing this week?"
baker ads meta insights --object act_123
# "Which campaigns are profitable?"
baker ads meta insights --object act_123 --level campaign --intent revenue --date-preset last_28d
# "Which creatives need a refresh?"
baker ads meta insights --object act_123 --level ad --intent ranking --date-preset last_14d
# "Where do users drop off the funnel?"
baker ads meta insights --object act_123 --intent funnel
# "Should we shift budget to Instagram?"
baker ads meta insights --object act_123 --level adset \
--breakdowns publisher_platform,platform_position
# "Why did spend drop yesterday?"
baker ads meta activities --account-id act_123 --days 7
# "What are we running right now?"
baker ads meta campaigns --account-id act_123 # ACTIVE only by default
baker ads meta ads --account-id act_123 --all-statuses # widen to everything
# "Tell me about ad 9988"
baker ads meta creatives --creative-id 9988
baker ads meta preview --creative-id 9988 --ad-format MOBILE_FEED_STANDARD --out-file /tmp/p.html
# "What audience is this targeting?"
baker ads meta audiences --account-id act_123
# "Is the pixel firing?"
baker ads meta pixels --account-id act_123
baker ads meta pixels --pixel-id 9988 --stats --days 7Smart defaults (so agents don't enumerate the API)
insights — the workhorse:
--level account(override:campaign|adset|ad)--intent baselinefield bundle. Intents available:baseline,revenue,funnel,ranking,video,identity. List with--list-intents.--date-preset last_7dunless you pass--since/--until.- Filter:
impressions > 0so undelivered rows don't pollute results. Pass--include-undeliveredto keep them. - Sort:
spenddescending — highest-impact rows first.--no-sortto disable. - Attribution windows:
7d_click, 1d_view(only1d_view, 1d_click, 7d_click, 28d_clickwork post Jan 2026). - Identity columns (
campaign_name,ad_name, etc.) appended automatically per--levelso rows are self-describing. - Auto-async when the query is heavy (level=ad with breakdowns over an account, >2 breakdowns, >90-day range). Pass
--asyncto force it or--no-asyncto refuse fallback.
Listings (campaigns/adsets/ads) default to effective_status=ACTIVE — pass --all-statuses to widen, or --effective-status ACTIVE,PAUSED for a custom set.
Pagination is auto-drained (no manual cursor handling).
Full command surface
accounts # ad accounts in the connected scope
accounts --include-all # every account the token can see (admin/debug)
account # single-account detail (currency, balance, business, status)
businesses # /me/businesses
campaigns # ACTIVE-only by default
adsets # filter by --campaign-id
ads # filter by --adset-id or --campaign-id
creatives # list per account, or fetch single via --creative-id
audiences # custom audiences (incl. lookalikes)
pixels # list, or --stats for one --pixel-id
activities # account audit log, ~90d retention
insights # see "Smart defaults" above
preview # iframe HTML for a creative or ad in a given ad_formatThe HTTP backend exposes more endpoints (catalogs, ad-studies, ad-images, labels, high-demand-periods, raw currency lookup). They're intentionally not surfaced as CLI commands because AI agents rarely need them. Hit them via curl against /api/ads/meta/* if needed.
Auth + caching
- Tokens auto-refresh server-side (60-day rolling long-lived user tokens). On
code 190reconnect Meta from the dashboard. - Account scoping: backend rejects any call against an account not in the picker selection — pick accounts via Settings → Connections → Meta Ads first.
- All cached actions are keyed per company; two companies that connect to the same Meta account never share cache entries.
--skip-cacheon any command forces a re-fetch.
Notes / gotchas
spendis a decimal string in account currency, not an integer.effective_status≠status. The dashboard shows effective_status (e.g.WITH_ISSUES,PENDING_REVIEW,DISAPPROVED,ADSET_PAUSED).- Meta creatives are effectively immutable once attached. Editing copy/image/CTA = create a new creative + reattach (writes are out of scope for now).
- Currency offsets are non-uniform (JPY/KRW = 1, KWD/BHD = 1000, most = 100). Read
account.currencybefore doing budget math.
LinkedIn Ads (baker ads linkedin)
LinkedIn Marketing API for B2B ad insights. Connect via OAuth in the dashboard, pick which ad accounts to scope to, then call from the CLI. Account ID via --account-id 503001492 (numeric) or --account-urn urn:li:sponsoredAccount:503001492, or set BAKER_LINKEDIN_AD_ACCOUNT_ID env.
LinkedIn's superpower over Meta/Google: firmographic pivots on every analytics query — pivot=job-title|company|industry|seniority|function|company-size. Meta has nothing like MEMBER_COMPANY returning company URNs of every account whose employees saw the ad. AI agents should reach for linkedin demographics and linkedin top-companies first when answering ABM/persona questions.
Reads + staged writes. The read surface is curated for AI agents per the LinkedIn Ads playbook (B2B operating manual covering audits, ABM, attribution, scaling). Write commands (campaign groups, campaigns, creatives, audiences, conversions, lead forms) are staged: they never hit the LinkedIn API when you run them — each op is fully validated at stage time, shows in the dashboard chat as a pending LinkedIn change, and applies only when the chat is published. See Staged writes below.
Common AI questions, mapped to commands
# "How is account X doing this week?"
baker ads linkedin analytics
# "Who are we reaching, by job title?" (the LinkedIn killer)
baker ads linkedin analytics --level campaign --campaign-id 1234 \
--pivot job-title --intent baseline --last-days 30
# "Top companies seeing this campaign" (ABM feedback loop) — org names auto-resolved
baker ads linkedin top-companies --campaign-id 1234 --last-days 30
# "Who are these org URNs?" — resolve org URNs to company names (probe leakage)
baker ads linkedin resolve --urns urn:li:organization:17719,urn:li:organization:19022
baker ads linkedin resolve --ids 17719,19022 --output csv
# "Who is in our audience by industry/seniority/function/title — all at once?"
baker ads linkedin demographics --campaign-id 1234 --last-days 30
# "Revenue by campaign over Q1"
baker ads linkedin analytics --level campaign --campaign-id 1,2,3 \
--intent revenue --start 2026-01-01 --end 2026-03-31 --granularity MONTHLY
# "How is the lead form converting?"
baker ads linkedin analytics --level campaign --campaign-id 1234 --intent lead-gen
# "Which creatives are fatigued?"
baker ads linkedin analytics --level creative --creative-id 1,2,3 --intent ranking
# "Run the playbook audit"
baker ads linkedin audit --account-id 503001492
baker ads linkedin audit --account-id 503001492 --format md # deliverable-ready table
# "Pull the leads from the last 7 days"
baker ads linkedin leads --account-id 503001492 --since-days 7
# "Is the Insight Tag healthy?"
baker ads linkedin conversions health --account-id 503001492
# "Pre-launch sanity check: is this audience too narrow?"
baker ads linkedin audience-size --account-id 503001492 --targeting-file targeting.json
# "What bid should I start at?"
baker ads linkedin bid-pricing --account-id 503001492 \
--objective LEAD_GENERATION --cost-type CPC --targeting-file targeting.jsonSmart defaults
analytics — the 3-axis workhorse:
--level account(override:campaign-group|campaign|creative)--intent baselinefield bundle. Intents:baseline | revenue | funnel | engagement | video | lead-gen | inmail | document | ranking | identity. List with--list-intents.--pivot none. Pivots:none | campaign | campaign-group | creative | company | account | conversion | job-title | job-function | seniority | industry | company-size | country | region | device | placement | serving-location | card-index | objective | conversation-node | conversation-node-button. List with--list-pivots.--granularity DAILY. Auto-forced toALLwhen pivoting on aMEMBER_*dim (LinkedIn rejects DAILY + demographic) — surfaced in stderr +warnings.--last-days 7unless you pass--start/--end.- Sort:
costInUsddescending.--no-sortto disable. - Derived metrics injected client-side:
ctr,cpc,cpm,frequency,leadCompletionRate— LinkedIn doesn't return these. - URN normalization: every URN field becomes
{type, id, urn}so rows are self-describing. - Wide windows (>180d) auto-segment into 30-day chunks and combine, since
q=analyticscaps at 15,000 rows with no pagination.
Listings (campaign-groups/campaigns/creatives) default to ACTIVE only — pass --all-statuses to widen, or --statuses ACTIVE,PAUSED for a custom set.
Pagination is auto-drained (start/count loop hidden).
Demographic pivots (MEMBER_*) come back delayed 12-24h with a ≥3-event privacy floor — small buckets are dropped. The CLI surfaces DELAYED_DEMOGRAPHIC and (when results are unexpectedly empty) BELOW_PRIVACY_FLOOR warnings.
Full command surface
accounts # accounts in the connected picker scope
accounts --include-all # every account the token can see
account # single-account detail (currency, type, status)
campaign-groups # default ACTIVE-only
campaigns # default ACTIVE-only; reveals audit-relevant settings
creatives # default ACTIVE intended-status
analytics # see "Smart defaults" above (3-axis: level × intent × pivot)
demographics # sweep all firmographic pivots in one call
top-companies # pivot=MEMBER_COMPANY shortcut (ABM)
conversation # pivot=CONVERSATION_NODE_BUTTON shortcut (Sponsored Messaging)
facets list # all targeting facets (industries, seniorities, titles, employers, …)
facets values --facet titles --query "..." # typeahead → URNs
audience-size # estimate reach for a targetingCriteria payload + sweet-spot warnings
bid-pricing # min/suggested/max bid + playbook §06 floor (2/3 of suggested)
forecast # adSupplyForecasts wrapper for greenfield planning
leads # Lead Gen Form responses (90d retention — sync to your CRM)
conversions list # conversion rules (Insight Tag + CAPI)
conversions health # 5-point Insight Tag / CAPI health check (playbook §07)
audit # 30+ playbook checks → severity-tagged findings
audit --format md # deliverable-ready markdown table
campaign-groups create|update|pause|resume|duplicate # staged writes (see below)
campaigns create|update|pause|resume|archive|duplicate
creatives create|update|pause|resume|duplicate
audiences create|upload
conversions create|update
lead-forms create|update
draft [remove <ref> | clear] # review/undo staged write opsStaged writes
Write commands never call the LinkedIn API at stage time. Each stages an op on the current chat's draft, fully validated at staging (field limits, enums, budget minimums, URN shapes, parent existence, Baker media readiness, status-transition legality). Validation errors are structured per-field; non-blocking warnings (Maximum Delivery when no --bid, budget below playbook floor, audience under 300 rows, >4 lead-form questions, audience expansion/LAN on) come back in the stage envelope. Ops apply when the chat is published.
If the company's LinkedIn-write flag is off (the default), publish runs the identical lifecycle simulated — fake urn:li:simulated:* results, zero LinkedIn calls. The stage response and baker ads linkedin draft show mode: "simulated".
# Chain creates via li_temp_* refs — group → campaign → creative in one chat
baker ads linkedin campaign-groups create --name "Q3 ABM" --total-budget 5000 --currency EUR # → li_temp_x1
baker ads linkedin campaigns create --name "ABM Tier-1" --group li_temp_x1 \
--objective WEBSITE_VISIT --type SPONSORED_UPDATES --cost-type CPC \
--bid 8.50 --daily-budget 75 --currency EUR --locale en_US --targeting-file targeting.json # → li_temp_x2
baker ads linkedin creatives create --campaign li_temp_x2 --format image \
--image-id <bakerImageId> --headline "Book a demo" --landing-url https://example.com/demo --cta REQUEST_DEMO
# Fix the classic audit findings
baker ads linkedin campaigns update 123456 --audience-expansion off --lan off
baker ads linkedin campaigns update 123456 --bid 7.50 --daily-budget 100 --currency USD
# Matched audiences (CSV with header; email columns SHA-256 hashed locally)
baker ads linkedin audiences create --name "ABM Tier-1" --type company-list --list-file accounts.csv
# Conversions with playbook-default windows
baker ads linkedin conversions create --name "Demo booked" --type LEAD --method INSIGHT_TAG \
--post-click-window 30 --view-window 7
# Safe duplication — stage-time read of the source → staged DRAFT create ("Duplicate ad", never "Link to original")
baker ads linkedin campaigns duplicate 123456 --name "New variant"
# Change copy/media/URL on a LIVE ad — direct-content ads (text/spotlight/follower/jobs) update IN PLACE;
# pass only the changed fields, the rest of the ad's current content carries over
baker ads linkedin creatives update 1458423674 --headline "New headline" --description "New copy."
# Post-based ads (image/video/carousel/…) sponsor a post whose content can't be edited — replace in ONE command:
# duplicate with overrides (unchanged fields + status carry over) + --replace pauses the original once the copy publishes
baker ads linkedin creatives duplicate 1458413484 --headline "New headline" --replace
# Amend staged ops in place — `update <li_temp_ref>` merges into the staged create and re-validates
baker ads linkedin creatives update li_temp_x3 --headline "Sharper headline" --image-id <bakerImageId>
baker ads linkedin campaigns update li_temp_x2 --daily-budget 100 --currency EUR
# A second update to the same real URN also merges into the already-staged update op.
# Review / undo before publish; after publish shows per-op results (applied/simulated/failed/skipped)
baker ads linkedin draft
baker ads linkedin draft remove li_temp_x2 # removing a create cascades to dependents
baker ads linkedin draft clearNotes:
- All write commands take
--file <json>payloads; explicit flags override file keys.baker schema ads.linkedin.campaigns.createfor exact args. - Money flags (
--bid,--daily-budget,--total-budget) require--currency. - Creative media comes from the Baker library (
--image-id/--video-idfrombaker images/baker videos— uploaded to LinkedIn at publish) or as LinkedIn URNs (--image-urn/--video-urn). Formats:image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs; complex formats take--filewith the full content object; conversation ads take--filewith the message flow ({message: {subject, body, senderName?, buttons[]}}— buttonsNESTED(withnestedMessage) orLANDING_PAGE(withlandingPageUrl), ≤25 messages, bodies ≤500 chars, labels ≤25). Limits: headline ≤70, text-ad 25/75, intro soft-truncates at 600 chars. TLA sponsors an existing post via--post-urn. - Lead forms are file-first (
lead-forms create --file form.json): name, headline (≤60), privacyPolicyUrl, questions[] (≤12; playbook: ≤4 for completion).
audit — playbook diagnostic
Runs in parallel: account detail + every campaign + every creative + conversion rules + account-level analytics + per-campaign frequency. Synthesizes findings across:
- Settings — Audience Expansion disabled, LinkedIn Audience Network disabled, Manual CPC bidding (not Maximum Delivery), daily budget ≥ $50, lifetime budget cap set, budget-fragmentation guard
- Tracking — at least one enabled rule, lead/purchase event configured, CAPI events recent (≤7d),
ONE_TIME_EACH_MEMBERfor leads, view-through window ≤7d, click attribution 7-90d - Creative — ≥3 ad formats per campaign, TLA presence, no creative active >90d, frequency <7, REJECTED creatives flagged
- Performance — account CTR ≥ 0.4%, LP CVR ≥ 2%, lead-form completion ≥ 10%, delivered impressions in window, spend > $0
- Hygiene — no abandoned DRAFT campaigns, every active campaign has at least one creative
Each finding: {id, area, check, status, severity, evidence, fix: {explanation, playbookRef}}. Filter with --severity critical,high or --area Settings,Tracking. The JSON summary counts pass/critical/high/medium/low/n_a.
Auth + caching
- OAuth tokens auto-refresh server-side. On 401, reconnect LinkedIn from dashboard → integrations.
- Account scoping: backend rejects calls against an account not in the picker selection.
- Pinned API version:
Linkedin-Version: 202604(declared inoauth/constants.ts). Bump as a coordinated change — LinkedIn deprecates monthly with a ~12-month support window. - Cache TTLs: accounts/account-detail 1h, listings 30m, analytics 15m–6h (depends on date range), facets 7d, urn-resolve 1h, bid-pricing/forecast 6h, audience-size 1h.
--skip-cache(server-side) and--no-cache(client-side, where supported) on any command.
LinkedIn-specific gotchas
- Demographic + DAILY = error. CLI auto-forces granularity=ALL when pivoting on
MEMBER_*and warns. - 3-event privacy floor. Small demographic buckets are dropped silently — widen the date range if results are sparse.
- 15,000-row cap on
q=analytics(no pagination). CLI auto-segments by month for wide windows. - Lead Gen Form 90-day retention. Sync via
baker ads linkedin leadsregularly or lose them. - LinkedIn can overspend daily budget by 50% — only
totalBudgetlifetime cap stops runaway spend (audit check). - CPC can look high ($8–14 NA baseline) but CPL competitive thanks to firmographic targeting. Use playbook §00 dynamic benchmarks (geo + industry multipliers) before judging.
- No native change-history API. Activity logs are not surfaced — playbook §24 calls this out as a gap.
Error codes
| HTTP | LinkedIn serviceErrorCode | CLI maps to | Action |
|------|---|---|---|
| 401 | 100 | UNAUTHORIZED | Reconnect LinkedIn in dashboard |
| 403 | — | FORBIDDEN | Token lacks scope or account role |
| 404 | 65604 | NOT_FOUND | Entity removed or not in scope |
| 410 | — | GONE | Entity deleted; URN no longer queryable |
| 426 | — | INTERNAL_ERROR | LinkedIn-Version deprecated; backend bump required |
| 429 | 101 | RATE_LIMITED | Auto-backoff with Retry-After; if you see this, retry budget exhausted |
| 400 | 65601 / 65603 | VALIDATION_ERROR | Fix request — see error.fix.explanation |
Google Analytics 4 (baker ga4)
GA4 commands for multi-channel audits. Playbook-aligned presets, property health audits, and free-form Data API queries.
baker ga4 properties
List accessible GA4 properties. Run this first to find property IDs.
baker ga4 propertiesbaker ga4 audit
Run all GA4 admin health checks at once. Checks data retention, Google Ads linkage, phantom conversions, audience defaults, data streams, and attribution settings.
baker ga4 audit
baker ga4 audit --property-id properties/123456789Returns raw config data plus playbook-aligned warnings:
{
"ok": true,
"data": { "property": {...}, "dataRetention": {...}, "conversionEvents": [...] },
"warnings": [
{ "code": "SHORT_RETENTION", "message": "Data retention is TWO_MONTHS — should be FOURTEEN_MONTHS" },
{ "code": "PHANTOM_CONVERSION", "message": "\"page_view\" is marked as a conversion event" }
]
}baker ga4 query
Run GA4 Data API reports. Presets are the primary interface; free-form dimensions/metrics is the escape hatch.
# Playbook presets
baker ga4 query --preset tracking-health
baker ga4 query --preset lp-performance --days 14
baker ga4 query --preset funnel-leakage --out funnel.csv
baker ga4 query --list-presets
# Free-form
baker ga4 query --dimensions "date,sessionSourceMedium" --metrics "sessions,conversions"Flags:
| Flag | Description |
|------|-------------|
| --preset | Named preset (see below) |
| --dimensions | Comma-separated GA4 dimension names |
| --metrics | Comma-separated GA4 metric names |
| --days | Lookback window in days (default: 30) |
| --start-date | Start date (YYYY-MM-DD or GA4 relative like "30daysAgo") |
| --end-date | End date (YYYY-MM-DD or "yesterday") |
| --limit | Max rows (default: 1000) |
| --property-id | GA4 property ID (auto-resolved if not provided) |
| --out | Write to file (.csv, .jsonl, .json) |
| --output | Format: json|csv|jsonl|md |
| --no-cache | Skip cache |
Presets:
| Preset | Playbook | What it answers |
|--------|----------|-----------------|
| tracking-health | [07] Discrepancy | Is GA4 tracking matching GAds? (>20% gap = broken) |
| lp-performance | [06] LP Diagnostic | Which LPs have bad UX? (<10s = message mismatch) |
| traffic-quality | [06] Warm Traffic | Is Smart Bidding cherry-picking returning users? |
| funnel-leakage | [06] Funnel Analysis | Where do users drop off in the funnel? |
| first-touch | First-Touch | What channels actually acquire new users? |
| traffic-overview | General | General traffic trends |
Google Search Console (baker gsc)
GSC commands for PPC-SEO arbitrage, brand halo analysis, and negative keyword discovery.
baker gsc sites
List verified Search Console sites.
baker gsc sitesbaker gsc query
Run Search Analytics queries. Presets are the primary interface.
# Playbook presets
baker gsc query --preset cannibalization
baker gsc query --preset brand-halo --brand "Acme" --days 90
baker gsc query --preset negative-keywords --out negatives.csv
baker gsc query --list-presets
# Free-form
baker gsc query --dimensions "query,page" --days 28
baker gsc query --dimensions "query" --row-limit 25000 --out keywords.csvFlags:
| Flag | Description |
|------|-------------|
| --preset | Named preset (see below) |
| --brand | Brand name (required for brand-halo preset) |
| --dimensions | Comma-separated: query, page, country, device, date |
| --days | Lookback window (default: 28) |
| --start-date / --end-date | Explicit date range |
| --row-limit | Max rows (default: 1000, max: 25000) |
| --type | Search type: web|image|video|news|discover |
| --site-url | Site URL (auto-resolved if not provided) |
| --out | Write to file |
| --output | Format: json|csv|jsonl|md |
| --no-cache | Skip cache |
Presets:
| Preset | Playbook | What it answers |
|--------|----------|-----------------|
| cannibalization | [05] PPC-SEO Arbitrage | Which queries rank #1-2 organically where you also pay? |
| missed-revenue | [05] Opportunity | High-impression queries with no PPC coverage? |
| brand-halo | [21] YouTube/DG ROI | Is upper-funnel spend driving brand searches? |
| negative-keywords | Negative Discovery | Queries to add as negative keywords |
| top-pages | General | Top performing pages |
| top-queries | General | Top search queries |
baker gsc sitemaps
Check sitemap health for a site.
baker gsc sitemaps
baker gsc sitemaps --site-url "https://example.com/"Competitive Intelligence (baker research)
Market and competitor research powered by DataForSEO and AI-powered Google Search grounding. Shows who's competing for keywords, search intent classification, competitor keyword strategies, keyword gaps, landing page performance, and open-ended AI research.
All DataForSEO research responses include a note field indicating that data is estimated from third-party SERP scraping and should be used for directional insights, not precise measurement.
baker research web "question"
Search the web with AI to answer any open-ended marketing question — competitors, ICP, pricing, pain points, market trends. Uses live internet data via Google Search.
baker research web "Who are the main competitors of HubSpot CRM?"
baker research web "What are the top pain points for SMB CRM buyers?" --depth high
baker research web "Full competitive analysis of project management SaaS" --depth xhigh
baker research web "What is the pricing of monday.com?" --output mdDepth levels:
| Depth | Model | Use case | Timeout |
|-------|-------|----------|---------|
| medium (default) | Gemini Flash + minimal thinking | Quick lookups | 3m |
| high | Gemini Flash + deep thinking | Thorough answers | 6m |
| xhigh | Gemini Deep Research | Exhaustive research (use sparingly) | 15m |
Response:
{
"ok": true,
"data": {
"answer": "## HubSpot CRM Competitors\n\n### 1. Salesforce\n...",
"sources": [
{ "title": "HubSpot vs Salesforce", "url": "https://..." }
]
},
"fields": {
"answer": "AI-generated research answer (markdown)",
"sources": "Array of {title, url} sources used for grounding"
}
}Flags:
| Flag | Description |
|------------|------------------------------------------------------|
| --depth | Research depth: medium (default), high, xhigh |
| --output | Format: json (default), md (raw markdown + sources) |
baker research advertisers "keyword"
Find domains competing for a keyword in Google SERPs.
baker research advertisers "running shoes"
baker research advertisers "crm software" --location uk --limit 10Response:
{
"ok": true,
"data": [
{ "domain": "www.adidas.com", "avg_position": 1, "rating": 99, "etv": 111872, "visibility": 1 }
],
"fields": {
"domain": "Competing domain",
"avg_position": "Average SERP position (1 = top)",
"rating": "Domain relevance rating (0-100)",
"etv": "Estimated traffic value (USD)",
"visibility": "SERP visibility score (0-1)"
}
}Flags:
| Flag | Description |
|--------------|--------------------------------------|
| --location | Country code (us, uk, es, de...) or numeric code. Defaults to us (United States) — always set for non-US markets |
| --language | Language code or name (en, spanish, french...). Defaults to en (English) — always set for non-English markets |
| --limit | Max results (default: 20) |
| --no-cache | Skip cache (6h TTL) |
| --output | Format: json|csv|md|jsonl |
Note: The response includes a
query_contextobject showing the actual location and language used, withdefaults_warningwhen defaults were applied.
baker research autocomplete "seed keyword"
Get Google Autocomplete suggestions for keyword expansion.
baker research autocomplete "running shoes"
baker research autocomplete "crm" --location uk --limit 5Response:
{
"ok": true,
"data": [
{ "suggestion": "running shoes for men" },
{ "suggestion": "running shoes near me" }
],
"fields": {
"suggestion": "Autocomplete suggestion from Google"
}
}Flags:
| Flag | Description |
|--------------|--------------------------------------|
| --location | Country code (us, uk, es, de...). Defaults to us |
| --language | Language code or name. Defaults to en |
| --limit | Max suggestions (default: 10, max: 20) |
| --no-cache | Skip cache (24h TTL) |
| --output | Format: json|csv|md|jsonl |
baker research relevant-pages "domain.com"
Get the top pages of a competitor domain with organic traffic data.
baker research relevant-pages "competitor.com"
baker research relevant-pages "competitor.com" --location es --limit 10Response:
{
"ok": true,
"data": [
{ "page": "https://competitor.com/best-product", "etv": 12500, "keywords": 340, "top_10": 85 }
],
"fields": {
"page": "Page URL",
"etv": "Estimated monthly organic traffic",
"keywords": "Total organic keywords the page ranks for",
"top_10": "Keywords in positions 1-10"
}
}Flags:
| Flag | Description |
|--------------|--------------------------------------|
| --location | Country code (us, uk, es, de...). Defaults to us |
| --language | Language code or name. Defaults to en |
| --limit | Max results (default: 20, max: 1000) |
| --no-cache | Skip cache (6h TTL) |
| --output | Format: json|csv|md|jsonl |
baker research intent "kw1,kw2,kw3"
Classify Google Search intent for keywords.
baker research intent "buy running shoes,best running shoes 2026,how to tie shoes"Response:
{
"ok": true,
"data": [
{ "keyword": "buy running shoes", "intent": "transactional", "probability": 0.96 }
],
"fields": {
"keyword": "The keyword analyzed",
"intent": "Primary Google Search intent: informational, navigational, commercial, transactional",
"probability": "Confidence score 0.0-1.0"
}
}Flags:
| Flag | Description |
|--------------|--------------------------------------|
| --language | Language code or name (en, spanish, french...). Defaults to en (English) — always set for non-English keywords |
| --no-cache | Skip cache (7d TTL) |
| --output | Format: json|csv|md|jsonl |
baker research keywords-for-site "domain.com"
Get keywords a competitor targets. Use --type paid to see only paid keywords, --type organic for organic only.
baker research keywords-for-site "competitor.com"
baker research keywords-for-site "competitor.com" --type paid --limit 20
baker research keywords-for-site "competitor.com" --type organic --location ukResponse:
{
"ok": true,
"data": [
{ "keyword": "running shoes online", "search_volume": 12000, "cpc": 1.85, "competition": "HIGH", "competition_index": 82 }
],
"fields": {
"keyword": "Keyword the site targets",
"search_volume": "Monthly search volume",
"cpc": "Cost per click in USD",
"competition": "LOW, MEDIUM, or HIGH",
"competition_index": "Competition score 0-100"
}
}Flags:
| Flag | Description |
|--------------|----------------------------------------------------------|
| --location | Country code (us, uk, es, de...) or numeric code. Defaults to us (United States) — always set for non-US markets |
| --language | Language code. Defaults to en (English) — always set for non-English markets |
| --sort | Sort: relevance, search_volume, competition, cpc |
| --type | Filter: paid, organic, all (default: all) |
| --limit | Max results (default: 50) |
| --no-cache | Skip cache (6h TTL) |
| --output | Format: json|csv|md|jsonl |
baker research keyword-gap "them.com" "us.com"
Keywords the competitor has that you don't.
baker research keyword-gap "competitor.com" "mysite.com"
baker research keyword-gap "competitor.com" "mysite.com" --type paid --limit 100
baker research keyword-gap "competitor.com" "mysite.com" --offset 50 --limit 50Response:
{
"ok": true,
"your_domain": "mysite.co