Back to Paid Ad Campaign Management

Developer Handover Pack

Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.

4 pagesPDF · Technical
FS-DOC-04Technical

Developer Handover Pack

Paid Ad Campaign Management

[YourCompany.com] · Marketing Department · Prepared by FullSpec · [Today's Date]

This document is the single source of truth for the FullSpec build team delivering the Paid Ad Campaign Management automation. It covers the current-state process map, full agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. The FullSpec team uses this pack to configure, connect, and test every component. Your team's responsibility is limited to granting API access, confirming budget threshold values, and validating the parallel-run output before go-live.

01Process snapshot

Step
Name
Description
1
Log Into Google Ads Dashboard
Marketing Manager opens Google Ads and reviews yesterday's spend, impressions, and conversions manually each working day. Time cost: 10 min/day.
2
Log Into Meta Ads Manager
Marketing Manager opens Meta Ads Manager separately to review Facebook and Instagram campaign performance. No combined view exists. Time cost: 10 min/day.
3
Copy Spend Data Into Spreadsheet
Daily spend, clicks, impressions, and conversions are manually copied from both platforms into the Google Sheets master log. Paste errors are common. Time cost: 20 min/day.
4
Check Against Budget Thresholds
Manager visually compares cumulative spend against monthly budget caps for each campaign with no automated alerts. Time cost: 15 min/day.
5
Adjust Budgets or Pause Campaigns
If a campaign is overspending or underperforming, the manager logs back into the relevant platform and manually adjusts or pauses it. Time cost: 15 min/day (as needed).
6
Update CRM With Lead Attribution
Conversions from ad platforms are manually cross-referenced with HubSpot contact records to verify paid-channel attribution. Frequently skipped due to time pressure. Time cost: 20 min/day.
7
Build Weekly Performance Report
Each Friday the manager assembles a performance summary by hand from the master spreadsheet, formats charts, and writes commentary. Time cost: 45 min/week.
8
Share Report With Stakeholders
The finished report is pasted into Slack or emailed. Version control is inconsistent. Time cost: 10 min/week.
Time cost summary: Manual time per daily cycle is 90 minutes (steps 1, 2, 3, 4, 5, 6) plus 55 minutes spread across the week for steps 7 and 8, totalling approximately 145 minutes of manual work per full cycle. Across a five-day working week this compounds to roughly 6 hours/week and $9,100/year at the assumed $30/hour rate. The automation replaces steps 1, 2, 3, 4, 6, 7, and 8 entirely. Step 5 (budget adjustment and campaign pausing) remains a human decision due to financial risk.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Ad Data Sync Agent

Connects to the Google Ads API and the Meta Marketing API on a daily schedule each morning. Authenticates via OAuth 2.0 for Google and a system user access token for Meta. Pulls spend, clicks, impressions, and conversions for all active campaigns on both platforms and merges the results into a single structured data object keyed by campaign ID and date. Writes each campaign row to the correct position in the Google Sheets master log using the Sheets API. No human intervention is required at any point in this agent's execution. If either API call fails, the agent retries up to three times before logging the error and halting without a partial write to avoid corrupt data in the master log.

Trigger
Daily schedule fires at 09:00 local time. Can also fire on a threshold breach event passed from an upstream monitoring step.
Tools
Google Ads API (v14+), Meta Marketing API (v18+), Google Sheets API (v4)
Replaces steps
Steps 1, 2, and 3 (platform logins and manual data copying)
Estimated build
12 hours, Moderate complexity
// Input
trigger_time: ISO8601 timestamp from scheduler
google_ads_customer_id: string (MCC or individual account ID)
meta_ads_account_id: string (act_XXXXXXXXXX format)
sheet_id: string (Google Sheets document ID)
sheet_tab: string (e.g. 'Daily Log')
date_range: { start: 'YYYY-MM-DD', end: 'YYYY-MM-DD' }

// Output
campaigns[]: {
  campaign_id: string,
  platform: 'google' | 'meta',
  campaign_name: string,
  date: 'YYYY-MM-DD',
  spend_usd: float,
  clicks: integer,
  impressions: integer,
  conversions: integer,
  cpc_usd: float,
  ctr_pct: float,
  roas: float | null
}
write_status: 'success' | 'partial' | 'failed'
rows_written: integer
error_log[]: { step: string, message: string, timestamp: ISO8601 }
  • Google Ads API requires a developer token approved at Standard Access level. Confirm this is granted before build starts. Test Access level does not support production data volumes.
  • Meta API requires a system user token created under the Business Manager, not a personal user token. Personal tokens expire and will break the daily schedule silently.
  • The Google Sheets master log must have a defined header row with column names matching the output field names exactly (campaign_id, platform, campaign_name, date, spend_usd, clicks, impressions, conversions, cpc_usd, ctr_pct, roas). Restructuring the sheet is part of the Access and Config Setup stage.
  • Dedupe logic: before writing, the agent checks whether a row with the same campaign_id and date already exists. If it does, it overwrites rather than appending a duplicate. This handles manual reruns and partial failures cleanly.
  • Google Ads GAQL query must filter on campaign.status = 'ENABLED' to avoid pulling paused campaigns and inflating row counts. Confirm whether paused campaigns should also be logged for audit purposes.
  • Meta API pagination must be handled explicitly. Accounts with more than 25 active campaigns will paginate and the agent must follow the next_page cursor until exhausted.
  • Retry policy: three attempts with exponential backoff (30 s, 90 s, 270 s) on any 429 or 5xx response before the agent marks the run as failed and writes to the error log table.
  • The Sheets write must use a batch update (batchUpdate endpoint), not sequential single-row writes, to stay within Sheets API quota limits for accounts with 20+ campaigns.
Campaign Analysis and Alert Agent

Fires immediately after the Ad Data Sync Agent confirms a successful write to the Google Sheets master log. Reads the freshly written rows, loads the budget and KPI threshold configuration from a dedicated config tab in the same sheet, and compares actual spend and conversion performance against those thresholds for each campaign. Scores each campaign as within range, approaching cap, or over cap. For any campaign flagged as anomalous, it composes and sends a structured Slack message to the configured marketing channel. It then writes conversion attribution data, matched by UTM parameters, to the corresponding HubSpot contact records via the HubSpot Contacts API. Finally, the Looker Studio dashboard refreshes automatically because it is connected directly to the Google Sheet as its data source and requires no separate API call from this agent.

Trigger
Fires on completion event from Ad Data Sync Agent (write_status = 'success'). Does not run if the upstream agent reports 'failed' or 'partial'.
Tools
Google Sheets API (v4), Slack API (Web API, chat.postMessage), HubSpot Contacts API (v3), Google Looker Studio (passive, data-source refresh)
Replaces steps
Steps 4, 6, 7, and 8 (threshold checking, CRM attribution update, report build, and stakeholder share)
Estimated build
14 hours, Moderate complexity
// Input
sheet_id: string (same document as Ad Data Sync Agent)
data_tab: string (e.g. 'Daily Log')
config_tab: string (e.g. 'Thresholds')
slack_channel_id: string (e.g. 'C0XXXXXXXXX')
hubspot_portal_id: string
campaigns[]: { campaign_id, platform, date, spend_usd, conversions, cpc_usd, roas }

// Threshold config row structure (read from config_tab)
threshold_row: {
  campaign_id: string,
  monthly_budget_usd: float,
  daily_spend_cap_usd: float,
  max_cpc_usd: float,
  min_roas: float | null,
  alert_pct: integer (e.g. 80 = alert at 80% of budget consumed)
}

// Output
anomaly_flags[]: {
  campaign_id: string,
  campaign_name: string,
  flag_type: 'budget_cap' | 'cpc_spike' | 'roas_drop' | 'zero_spend',
  current_value: float,
  threshold_value: float,
  recommended_action: string
}
slack_messages_sent: integer
hubspot_records_updated: integer
hubspot_errors[]: { contact_id: string, error: string }

// On approval (manager acts on Slack alert)
manager_action: 'pause_campaign' | 'adjust_budget' | 'no_action'
actioned_by: string (Slack user display name, logged to sheet)
action_timestamp: ISO8601
  • The threshold configuration table in Google Sheets is the single source of truth for all alert logic. Column headers must be exactly: campaign_id, monthly_budget_usd, daily_spend_cap_usd, max_cpc_usd, min_roas, alert_pct. The build team will create this tab during the Access and Config Setup stage.
  • HubSpot attribution match relies on UTM parameters (utm_source, utm_medium, utm_campaign) being consistently applied to all ad destination URLs. If UTMs are absent or inconsistent, the matching query will return zero results and the step will log a warning rather than failing the run. A UTM audit is required before go-live.
  • HubSpot API scope required: crm.objects.contacts.write and crm.objects.deals.read. Confirm the HubSpot account tier supports private app tokens (Starter tier and above). Free tier does not support private apps.
  • Slack bot must be installed in the workspace with the chat:write scope and invited to the target marketing channel before build. The channel ID (not name) must be stored in the credential store to avoid breakage if the channel is renamed.
  • Anomaly scoring logic: a campaign is flagged if spend_usd exceeds (monthly_budget_usd multiplied by alert_pct divided by 100), or if cpc_usd exceeds max_cpc_usd, or if roas falls below min_roas (where min_roas is not null). Zero-spend on a campaign with impressions greater than zero is also flagged as a potential tracking issue.
  • Looker Studio refresh is passive, it reads from the Google Sheet on each dashboard open. No API call is needed from this agent. Confirm the Looker Studio report is connected to the correct sheet tab and using the correct date dimension field.
  • Dedupe on HubSpot writes: identify the contact by email address extracted from the UTM-tagged conversion event. If no matching contact is found, log the unmatched conversion to a separate sheet tab named 'Unmatched Conversions' rather than creating a new HubSpot record silently.
  • Slack alert message must include: campaign name, platform, flag type, current value, threshold value, and recommended action. Include a direct link to the campaign in the ad platform where possible to reduce the manager's time to act.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full trigger-to-output data flow with field names at each agent handoff
// ─────────────────────────────────────────────────────────────────
// TRIGGER
// ────────────���────────────────────────────────────────────────────
scheduler.fire({ time: '09:00', date: 'YYYY-MM-DD' })
  OR threshold_monitor.breach({
    campaign_id: string,
    metric: 'spend_pct' | 'cpc_usd' | 'roas',
    current_value: float,
    threshold_value: float
  })

// ─────────────────────────────────────────────────────────────────
// AGENT 1: Ad Data Sync Agent
// ─────────────────────────────────────────────────────────────────

// Step 1 — Google Ads API query
google_ads.query({
  customer_id: google_ads_customer_id,
  query: "SELECT campaign.id, campaign.name, metrics.cost_micros,
           metrics.clicks, metrics.impressions, metrics.conversions,
           metrics.average_cpc, metrics.ctr
           FROM campaign
           WHERE segments.date = '{yesterday}'
           AND campaign.status = 'ENABLED'"
})
-> google_raw[]: {
     campaign_id, campaign_name, cost_micros,
     clicks, impressions, conversions, average_cpc, ctr
   }

// Step 2 — Normalise Google fields
google_normalised[]: {
  campaign_id: string,
  platform: 'google',
  campaign_name: string,
  date: 'YYYY-MM-DD',
  spend_usd: cost_micros / 1_000_000,
  clicks: integer,
  impressions: integer,
  conversions: integer,
  cpc_usd: average_cpc / 1_000_000,
  ctr_pct: ctr * 100,
  roas: null  // Google Ads ROAS requires separate revenue signal if not configured
}

// Step 3 — Meta Marketing API query (runs in parallel with Step 1)
meta_ads.insights({
  account_id: meta_ads_account_id,
  fields: ['campaign_id', 'campaign_name', 'spend', 'clicks',
           'impressions', 'actions', 'cpc', 'ctr', 'purchase_roas'],
  date_preset: 'yesterday',
  level: 'campaign'
})
-> meta_raw[]: {
     campaign_id, campaign_name, spend, clicks,
     impressions, actions[], cpc, ctr, purchase_roas[]
   }

// Step 4 — Normalise Meta fields
meta_normalised[]: {
  campaign_id: string,
  platform: 'meta',
  campaign_name: string,
  date: 'YYYY-MM-DD',
  spend_usd: parseFloat(spend),
  clicks: integer,
  impressions: integer,
  conversions: actions.find(a => a.action_type === 'lead').value | 0,
  cpc_usd: parseFloat(cpc),
  ctr_pct: parseFloat(ctr),
  roas: purchase_roas[0].value | null
}

// Step 5 — Merge and dedupe
merged_campaigns[]: [...google_normalised, ...meta_normalised]
// Dedupe check: sheet lookup by (campaign_id + date) before write
// If row exists: overwrite. If not: append.

// Step 6 — Write to Google Sheets master log
sheets.batchUpdate({
  sheet_id: sheet_id,
  tab: 'Daily Log',
  rows: merged_campaigns[]  // one row per campaign per date
})
-> write_status: 'success' | 'partial' | 'failed'
-> rows_written: integer

// ─────────────────────────────────────────────────────────────────
// AGENT HANDOFF: Ad Data Sync Agent -> Campaign Analysis and Alert Agent
// Condition: write_status === 'success'
// Payload: { sheet_id, data_tab, config_tab, campaigns: merged_campaigns[] }
// ─────────────────────────────────────────────────────────────────

// ─────────────────────────────────────────────────────────────────
// AGENT 2: Campaign Analysis and Alert Agent
// ─────────────────────────────────────────────────────────────────

// Step 7 — Load threshold config
sheets.read({ sheet_id: sheet_id, tab: 'Thresholds' })
-> thresholds[]: {
     campaign_id, monthly_budget_usd, daily_spend_cap_usd,
     max_cpc_usd, min_roas, alert_pct
   }

// Step 8 — Score each campaign
for each campaign in merged_campaigns[]:
  t = thresholds.find(t => t.campaign_id === campaign.campaign_id)
  flags = []
  if campaign.spend_usd > t.daily_spend_cap_usd:
    flags.push({ flag_type: 'budget_cap', current: spend_usd, threshold: daily_spend_cap_usd })
  if campaign.cpc_usd > t.max_cpc_usd:
    flags.push({ flag_type: 'cpc_spike', current: cpc_usd, threshold: max_cpc_usd })
  if t.min_roas != null AND campaign.roas < t.min_roas:
    flags.push({ flag_type: 'roas_drop', current: roas, threshold: min_roas })
  if campaign.impressions > 0 AND campaign.conversions === 0 AND campaign.spend_usd > 50:
    flags.push({ flag_type: 'zero_conversions' })

// Step 9 — Send Slack alerts for flagged campaigns
for each flagged campaign:
  slack.chat.postMessage({
    channel: slack_channel_id,
    text: ':warning: Campaign Alert: {campaign_name} ({platform})',
    blocks: [
      { flag_type, current_value, threshold_value,
        recommended_action, platform_deep_link }
    ]
  })
-> slack_messages_sent: integer

// Step 10 — HubSpot attribution write
// Source: conversion events matched by utm_campaign === campaign_name
for each campaign with conversions > 0:
  hubspot.contacts.search({
    filter: { property: 'hs_analytics_last_url', value contains utm_campaign }
  })
  -> matching_contacts[]: { contact_id, email, hs_analytics_last_url }
  for each matching_contact:
    hubspot.contacts.update({
      contact_id: contact_id,
      properties: {
        hs_latest_source: 'PAID_SEARCH' | 'PAID_SOCIAL',
        hs_latest_source_data_1: campaign_name,
        hs_latest_source_data_2: platform,
        last_attribution_date: date
      }
    })
  // Unmatched conversions -> append to 'Unmatched Conversions' sheet tab
-> hubspot_records_updated: integer
-> hubspot_errors[]: { contact_id, error }

// Step 11 — Looker Studio refresh (passive)
// Looker Studio reads directly from 'Daily Log' tab on each dashboard load.
// No API call required. Data is live as soon as the Sheets write completes.

// ─────────────────────────────────────────────────────────────────
// TERMINAL STATE
// ─────────────────────────────────────────────────────────────────
run_summary: {
  run_date: 'YYYY-MM-DD',
  campaigns_synced: integer,
  rows_written: integer,
  anomalies_flagged: integer,
  slack_messages_sent: integer,
  hubspot_records_updated: integer,
  errors: error_log[]
}
// run_summary written to 'Run Log' sheet tab and to Supabase error_log table
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation platform with native HTTP/REST nodes and credential store. One workflow per agent (two workflows total): 'Ad Data Sync' and 'Campaign Analysis and Alert'. Workflows share a single credential store for all API keys and tokens. No cross-workflow data passing via external storage: the handoff payload is passed as a direct trigger event from workflow 1 to workflow 2 on successful completion.
Webhook configuration
A webhook endpoint is exposed by the orchestration layer to receive threshold breach events from any external monitoring step or future alerting integration. The scheduler trigger uses a built-in CRON node set to '0 9 * * 1-5' (09:00, Monday to Friday). The webhook must be secured with a shared secret header (X-FullSpec-Secret) validated at the entry node. The endpoint URL is registered in the Google Sheets config tab so it can be reused for manual reruns without build access.
Templating approach
Slack message blocks use a JSON block-kit template stored as a static file in the orchestration layer's file store. The template is populated at runtime with campaign_name, platform, flag_type, current_value, threshold_value, and recommended_action fields. HubSpot property update payloads use a reusable mapping object defined once in the workflow and referenced per contact update call. Google Sheets row structure is defined by a header-row mapping array that drives both the read and write operations, keeping the column order change-safe.
Error logging
All error events (API failures, dedupe conflicts, unmatched HubSpot contacts, partial Sheets writes) are written to a Supabase table named 'automation_error_log' with columns: id (uuid), workflow_name (text), step_name (text), error_message (text), payload_snapshot (jsonb), created_at (timestamptz). A Supabase database webhook triggers a Slack alert to the #automation-alerts channel (separate from the marketing channel) whenever a new row is inserted with error severity 'critical'. Non-critical warnings are batched and included in the run_summary written to the 'Run Log' sheet tab.
Testing approach
All development runs against sandbox credentials first: a Google Ads test account (test customer ID), a Meta test ad account (available under Business Manager), and a staging copy of the Google Sheet with synthetic campaign rows. HubSpot sandbox (available on all paid tiers) is used for contact write testing. Slack alerts during testing are routed to a private #automation-test channel. Once sandbox tests pass, a five-day parallel run against live credentials with the production sheet is conducted before the manual process is retired. Discrepancies between automated and manual figures above 1% are treated as blocking issues.
Estimated total build time
Ad Data Sync Agent: 12 hours. Campaign Analysis and Alert Agent: 14 hours. End-to-end integration testing and parallel-run validation: 2 hours. Total: 28 hours, matching the confirmed build scope.
Credential requirements before build can start: (1) Google Ads developer token at Standard Access, plus OAuth 2.0 client credentials (client_id, client_secret, refresh_token) scoped to https://www.googleapis.com/auth/adwords. (2) Meta system user access token with ads_read permission, generated under the Business Manager system user. (3) Google Sheets OAuth 2.0 credentials scoped to https://www.googleapis.com/auth/spreadsheets. (4) HubSpot private app token with scopes crm.objects.contacts.write and crm.objects.deals.read. (5) Slack bot token with scope chat:write, installed to the workspace and invited to both the marketing channel and the #automation-alerts channel. (6) Supabase project URL and service role key for error log writes. All credentials are stored in the orchestration layer's encrypted credential store and are never hard-coded in workflow nodes. Contact support@gofullspec.com to confirm credential handover format before the build start date.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Paid Ad Campaign Management.

Launch Plan
Operations · Owner
View
ROI and Business Case
Finance · Owner
View
Process Runbook / SOP
Operations · Owner
View
Integration and API Spec
Technical · Developer
View
Test and QA Plan
Quality · Developer
View