Back to Campaign Performance Reporting

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

Campaign Performance Reporting

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

This document gives the FullSpec build team everything needed to construct, wire, and validate the Campaign Performance Reporting automation. It covers the current-state process map with bottleneck identification, full agent specifications with IO contracts, the end-to-end data flow trace, and the recommended build stack. The process owner's responsibilities are limited to providing API credentials, reviewing draft commentary during QA, and confirming go-live readiness. Everything else is handled by the FullSpec team.

01Process snapshot

Step
Name
Description
1
Pull Meta Ads Export
Marketing Manager logs into Meta Ads Manager, sets the correct date range, selects required columns, and downloads a CSV for each active campaign or ad set. Time cost: 20 min/cycle.
2
Pull Google Analytics Export
Marketing Manager opens Google Analytics, navigates to the relevant report view, applies date filters, and exports the data as a spreadsheet. Column names differ from the Meta export and require renaming. Time cost: 15 min/cycle.
3
Export HubSpot Lead and Contact Data
Marketing Manager runs a contact or deal report in HubSpot filtered by campaign source and date, then exports it. UTM attribution rarely matches ad platform numbers exactly, requiring manual reconciliation. Time cost: 20 min/cycle.
4
Consolidate Data into Master Spreadsheet
All three exports are copied into a master Google Sheet. Column headers are standardised, date formats corrected, and duplicate rows from overlapping date windows removed by hand. Time cost: 35 min/cycle.
5
Calculate Derived Metrics
Marketing Manager writes or updates formulas for cost per lead, click-through rate, conversion rate, and return on ad spend. Formula errors from pasted values must be found and fixed manually. Time cost: 20 min/cycle.
6
Update Charts and Tables
Charts linked to data ranges are checked and manually refreshed if source ranges have shifted. New rows sometimes break chart data references and must be corrected. Time cost: 15 min/cycle.
7
Write Summary Commentary
A short written summary of the week's highlights, underperforming campaigns, and recommended actions is typed into the report or a separate message. The highest-value step and often the most rushed. Time cost: 20 min/cycle.
8
Distribute Report to Stakeholders
The finished report link or PDF export is posted in the relevant Slack channel. The marketer tags relevant people and pastes top-line numbers manually. Time cost: 10 min/cycle.
Time cost summary: Total manual time per cycle is 155 minutes (2 hrs 35 min). At approximately 4 cycles per month (weekly cadence plus one monthly summary), this equals roughly 620 minutes (10+ hours) of manual effort per month and approximately 4 hours per week across the full year. Steps 1, 2, 3, 4, 5, and 6 are fully replaced by the Data Collection Agent and the Normalisation and Insight Agent. Step 8 is replaced by the Distribution Agent. Step 7 (commentary writing) is assisted by the Normalisation and Insight Agent, which produces a draft, but a human review gate remains before distribution. The net human task after automation is approximately 10 to 20 minutes of commentary review per cycle.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Data Collection Agent

Connects to Meta Ads Manager, Google Analytics, and HubSpot on a schedule and retrieves all campaign metrics for the reporting period without any manual exports or logins. The agent fires at the scheduled trigger time, authenticates with each platform via OAuth or API key, fetches raw metric data for the configured date window, and writes a structured staging dataset to a designated tab in the master Google Sheet. It must handle partial API failures gracefully, logging which source failed and retrying before alerting.

Trigger
Scheduled cron: weekly (Monday 07:00 local time) and monthly (1st of month 07:00 local time). Both schedules fire the same agent; a parameter flag distinguishes report type.
Tools
Meta Ads Manager (Marketing API v18+), Google Analytics (Data API v1beta), HubSpot (Contacts and Deals API v3)
Replaces steps
Steps 1, 2, and 3
Estimated build
9 hours — Complex
// Input
report_type: 'weekly' | 'monthly'
date_range: { start: ISO8601, end: ISO8601 }
campaign_ids: string[]   // pulled from config sheet

// Output (written to Google Sheets staging tab 'raw_data')
meta_rows: [{ campaign_id, campaign_name, impressions, clicks, spend_usd, conversions, date_range }]
ga_rows:   [{ session_source, sessions, goal_completions, bounce_rate, utm_campaign, date_range }]
hs_rows:   [{ contact_id, deal_id, campaign_source, lifecycle_stage, create_date, deal_value_usd }]
collection_status: { meta: 'ok'|'error', ga: 'ok'|'error', hs: 'ok'|'error' }
collected_at: ISO8601
  • Meta Marketing API: requires a verified Business Manager account with the 'ads_read' permission scope and an approved app. If the account has not previously used the API, app review may add 2 to 5 business days. Confirm this is in place before build begins.
  • Google Analytics: use a service account with 'Viewer' role on the GA4 property. The Data API v1beta quota is 10 concurrent requests and 10,000 requests per day per property. All queries must batch dimensions and metrics within a single request to avoid hitting quota.
  • HubSpot: use a Private App token (not legacy API key). Required scopes are 'crm.objects.contacts.read' and 'crm.objects.deals.read'. HubSpot UTM attribution uses last-touch model; the output must preserve both the HubSpot-attributed source and the raw UTM value so the Normalisation Agent can surface both without overwriting either.
  • Dedupe logic: the staging tab is wiped and rewritten on each run. Do not append; always replace. This prevents duplicate rows from overlapping date windows.
  • Fallback behaviour: if any single source returns an API error after three retry attempts (exponential backoff, max 90 seconds), mark that source status as 'error', write partial data for the other two sources, and trigger a Slack alert to the designated ops channel before halting. Do not proceed to the Normalisation Agent on a partial failure without explicit acknowledgement.
  • Campaign ID list must be maintained in a config tab named 'agent_config' in the master Google Sheet. The agent reads this tab at runtime so the owner can add or remove campaigns without a code change.
  • Confirm the exact Google Sheet ID and tab names with the process owner before build.
Normalisation and Insight Agent

Takes the raw multi-source dataset written by the Data Collection Agent, resolves column name mismatches, standardises date formats, deduplicates any overlapping rows, calculates derived KPIs, flags campaigns that are underperforming against configured targets, and drafts a plain-English summary paragraph for the report. The draft commentary identifies the top performer, the biggest underperformer, and one recommended action. The agent then presents the draft to the marketing manager for review before the Distribution Agent fires.

Trigger
Fires automatically once the Data Collection Agent sets collection_status to all 'ok' and writes a completion flag to the 'agent_config' tab cell B2.
Tools
Google Sheets (read and write), Google Looker Studio (data source refresh via Sheets connection)
Replaces steps
Steps 4, 5, and 6; partially replaces step 7 (drafts commentary, human reviews)
Estimated build
8 hours — Moderate
// Input (read from Google Sheets tab 'raw_data')
meta_rows: [{ campaign_id, campaign_name, impressions, clicks, spend_usd, conversions, date_range }]
ga_rows:   [{ session_source, sessions, goal_completions, bounce_rate, utm_campaign, date_range }]
hs_rows:   [{ contact_id, deal_id, campaign_source, lifecycle_stage, create_date, deal_value_usd }]
target_thresholds: { cpl_target_usd, roas_target, ctr_target_pct }  // from 'agent_config' tab

// Output (written to Google Sheets tab 'normalised_report')
normalised_rows: [{ campaign_id, campaign_name, impressions, clicks, spend_usd, conversions,
                    cpl_usd, ctr_pct, conversion_rate_pct, roas,
                    hs_leads, hs_deals, hs_deal_value_usd,
                    utm_campaign, underperformance_flag: boolean,
                    underperformance_reason: string | null }]
draft_commentary: string   // plain-English paragraph, max 120 words
top_performer: { campaign_id, campaign_name, metric: string, value: number }
biggest_underperformer: { campaign_id, campaign_name, metric: string, value: number }
recommended_action: string
normalisation_status: 'awaiting_review' | 'approved' | 'edited'
normalised_at: ISO8601

// On approval (manager sets normalisation_status to 'approved' or 'edited' in 'agent_config' tab B3)
Distribution Agent trigger fires
  • Column standardisation map must be hardcoded in the agent config and version-controlled. Meta uses 'campaign_name'; HubSpot uses 'hs_campaign_source'; GA uses 'sessionDefaultChannelGroup'. All must resolve to 'campaign_name' in the output.
  • Date format normalisation: all three sources may return dates in different formats (MM/DD/YYYY, YYYY-MM-DD, Unix timestamp). Normalise all to ISO8601 YYYY-MM-DD before any join or calculation.
  • Derived KPI formulas: CPL = spend_usd / hs_leads; CTR = clicks / impressions; conversion_rate = conversions / clicks; ROAS = (hs_deal_value_usd / spend_usd). Protect against division-by-zero with null coalescing.
  • Underperformance flag: a campaign is flagged if any of CPL exceeds cpl_target_usd, ROAS falls below roas_target, or CTR falls below ctr_target_pct. Thresholds are read from the 'agent_config' tab so the owner can adjust them without a code change.
  • Commentary generation uses an LLM call (prompt template stored in 'agent_config' tab). The prompt must include campaign names, top/bottom performer metrics, and the underperformance reasons. Output must be capped at 120 words. Include a fallback static template if the LLM call fails.
  • Looker Studio refresh: the agent must confirm the master Google Sheet's data connection to Looker Studio is live before writing. The refresh is triggered by the Sheets write itself; no direct Looker Studio API call is needed. Document this dependency clearly for the process owner.
  • The human review gate is implemented as a watch on 'agent_config' tab cell B3. The Distribution Agent polls this cell every 5 minutes. Timeout after 4 hours: if no approval is received, send a reminder Slack DM to the marketing manager.
Distribution Agent

Formats the final approved summary into a structured Slack message with key metric callouts, underperformance flags highlighted in the message body, and a direct link to the Looker Studio dashboard. Posts the message to the designated Slack channel and records the delivery timestamp. This agent fires only after the marketing manager has approved or edited the draft commentary, ensuring no report is distributed without human sign-off on the narrative.

Trigger
Fires when 'agent_config' tab cell B3 is set to 'approved' or 'edited' by the marketing manager, confirmed by the polling watch in the Normalisation Agent's review gate.
Tools
Slack (Web API, chat.postMessage), Google Looker Studio (dashboard URL reference only)
Replaces steps
Step 8
Estimated build
4 hours — Simple
// Input
normalised_rows: [{ campaign_id, campaign_name, spend_usd, cpl_usd, roas, ctr_pct, underperformance_flag, underperformance_reason }]
draft_commentary: string   // approved or edited version from 'agent_config' tab B4
top_performer: { campaign_id, campaign_name, metric, value }
biggest_underperformer: { campaign_id, campaign_name, metric, value }
looker_dashboard_url: string   // stored in 'agent_config' tab B5
slack_channel_id: string       // stored in 'agent_config' tab B6
report_type: 'weekly' | 'monthly'

// Output
slack_message_ts: string   // Slack message timestamp, stored in 'normalised_report' tab column N
delivery_status: 'sent' | 'failed'
delivered_at: ISO8601
  • Slack app requires the 'chat:write' and 'chat:write.public' OAuth scopes. The Slack Bot Token must be stored in the shared credential store, not in the Sheet. Confirm the target channel ID with the process owner before build.
  • Message format: use Slack Block Kit. The top block is a header with the report type and date range. The second block lists top-line metrics as fields (spend, CPL, ROAS, CTR). The third block contains the approved commentary paragraph. The fourth block lists underperforming campaigns as a bulleted warning section (only present if at least one underperformance_flag is true). The final block is a button linking to the Looker Studio dashboard.
  • No personal contact data (names, emails, phone numbers) from HubSpot should appear in the Slack message. The message must reference aggregate metrics only.
  • If the Slack API call fails, retry once after 30 seconds. On second failure, log the error to the Supabase error table and send a direct Slack DM to the ops contact (channel ID stored in 'agent_config' tab B7).
  • Store the returned slack_message_ts in the 'normalised_report' tab for audit purposes. This allows the team to trace which Slack post corresponds to which report run.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full data flow: Campaign Performance Reporting — trigger to Slack delivery
// ─────────────────────────────────────────────────────────────────
// TRIGGER
// ─────────────────────────────────────────────────────────────────
cron_schedule: 'weekly' | 'monthly'
  -> emit: { report_type, date_range: { start: ISO8601, end: ISO8601 } }
  -> read: campaign_ids[]        from GoogleSheets['agent_config'].B10:B50
  -> read: target_thresholds     from GoogleSheets['agent_config'].C2:C4

// ─────────────────────────────────────────────────────────────────
// AGENT 1: Data Collection Agent
// ─────────────────────────────────────────────────────────────────
MetaAdsManager.GET /v18.0/act_{ad_account_id}/insights
  params: { date_preset: custom, time_range: {since, until},
            fields: 'campaign_id,campaign_name,impressions,clicks,spend,actions',
            level: 'campaign' }
  -> meta_rows[]: { campaign_id, campaign_name, impressions,
                    clicks, spend_usd, conversions, date_range }

GoogleAnalytics.POST /v1beta/{property_id}:runReport
  body: { dateRanges: [{startDate, endDate}],
          dimensions: ['sessionDefaultChannelGroup','sessionCampaignName'],
          metrics: ['sessions','goalCompletionsAll','bounceRate'] }
  -> ga_rows[]: { session_source, sessions, goal_completions,
                  bounce_rate, utm_campaign, date_range }

HubSpot.GET /crm/v3/objects/contacts
  params: { filterGroups: [{ filters: [{ propertyName: 'hs_analytics_source',
            operator: 'EQ', value: 'PAID_SOCIAL' }] }],
            properties: 'hs_campaign_source,lifecycle_stage,createdate',
            after: epoch_ms(date_range.start) }
HubSpot.GET /crm/v3/objects/deals
  params: { properties: 'dealname,campaign_source,amount,createdate' }
  -> hs_rows[]: { contact_id, deal_id, campaign_source,
                  lifecycle_stage, create_date, deal_value_usd }

// Write raw output to staging
GoogleSheets.WRITE tab='raw_data'
  fields: meta_rows, ga_rows, hs_rows, collection_status, collected_at
GoogleSheets.SET 'agent_config'.B2 = 'collection_complete'

// ── HANDOFF: Data Collection Agent -> Normalisation and Insight Agent ──
// Trigger: 'agent_config'.B2 == 'collection_complete'

// ─────────────────────────────────────────────────────────────────
// AGENT 2: Normalisation and Insight Agent
// ─────────────────────────────────────────────────────────────────
GoogleSheets.READ tab='raw_data'
  -> resolve column_name_map:
     meta:'campaign_name' = ga:'sessionCampaignName' = hs:'campaign_source'
  -> normalise date formats to ISO8601 YYYY-MM-DD
  -> deduplicate on (campaign_id, date_range)
  -> join on: campaign_name (fuzzy match, Levenshtein distance <= 2)

// Derived KPI calculations
cpl_usd            = spend_usd / hs_leads
ctr_pct            = (clicks / impressions) * 100
conversion_rate_pct = (conversions / clicks) * 100
roas               = hs_deal_value_usd / spend_usd

// Underperformance flagging
underperformance_flag = (cpl_usd > target_thresholds.cpl_target_usd)
                      | (roas < target_thresholds.roas_target)
                      | (ctr_pct < target_thresholds.ctr_target_pct)

// Commentary generation
LLM.call prompt_template(top_performer, biggest_underperformer,
                          recommended_action, underperforming_campaigns)
  -> draft_commentary: string (max 120 words)

// Write normalised output
GoogleSheets.WRITE tab='normalised_report'
  fields: normalised_rows[], draft_commentary, top_performer,
          biggest_underperformer, recommended_action, normalised_at
GoogleSheets.SET 'agent_config'.B3 = 'awaiting_review'
GoogleSheets.SET 'agent_config'.B4 = draft_commentary

// Human review gate
POLL 'agent_config'.B3 every 5 min
  -> if B3 == 'approved' | 'edited': proceed
  -> if 4 hours elapsed with no approval: send Slack DM reminder

// ── HANDOFF: Normalisation and Insight Agent -> Distribution Agent ──
// Trigger: 'agent_config'.B3 set to 'approved' or 'edited'

// ─────────────────────────────────────────────────────────────────
// AGENT 3: Distribution Agent
// ─────────────────────────────────────────────────────────────────
GoogleSheets.READ tab='normalised_report'
  fields: normalised_rows[], top_performer, biggest_underperformer
GoogleSheets.READ 'agent_config'.B4  // approved commentary
GoogleSheets.READ 'agent_config'.B5  // looker_dashboard_url
GoogleSheets.READ 'agent_config'.B6  // slack_channel_id

// Format Slack Block Kit message
SlackBlock.header:  report_type + date_range
SlackBlock.fields:  spend_usd | cpl_usd | roas | ctr_pct
SlackBlock.section: approved_commentary
SlackBlock.section: underperforming_campaigns[] (if any flagged)
SlackBlock.button:  'View Full Dashboard' -> looker_dashboard_url

Slack.POST /api/chat.postMessage
  params: { channel: slack_channel_id, blocks: SlackBlocks }
  -> slack_message_ts: string

// Write delivery confirmation
GoogleSheets.SET 'normalised_report'.N{row} = slack_message_ts
GoogleSheets.SET 'normalised_report'.O{row} = delivered_at (ISO8601)

// ─────────────────────────────────────────────────────────────────
// ERROR PATH (any agent)
// ─────────────────────────────────────────────────────────────────
ON error:
  Supabase.INSERT table='automation_errors'
    { agent_name, error_code, error_message, payload_snapshot, occurred_at }
  Slack.POST channel=ops_channel_id
    { text: 'Reporting automation error: {agent_name} - {error_message}' }
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation platform with one workflow per agent (three workflows total): 'DCA - Data Collection', 'NIA - Normalisation and Insight', 'DA - Distribution'. All three workflows share a single credential store. No credentials are stored in Google Sheets or hardcoded in workflow nodes.
Webhook configuration
No inbound webhooks are required for this build. All triggers are schedule-based (cron) or polling-based (Sheets cell watch). Outbound calls to Meta Marketing API, Google Analytics Data API, HubSpot CRM API, and Slack Web API are authenticated via stored credentials. The review gate between Agent 2 and Agent 3 is implemented as a polling loop on the 'agent_config' tab cell B3, checking every 5 minutes with a 4-hour timeout before sending a reminder.
Templating approach
Slack message blocks are built using a Slack Block Kit JSON template stored as a workflow variable. The LLM commentary prompt template is stored in 'agent_config' tab cell D2 so the process owner can adjust tone or focus without a code change. Column name mapping and KPI formula definitions are stored in 'agent_config' tab columns E and F respectively, one row per field, making them auditable and editable without touching workflow logic.
Error logging
All agent errors are written to a Supabase table named 'automation_errors' with columns: id (uuid), agent_name (text), error_code (text), error_message (text), payload_snapshot (jsonb), occurred_at (timestamptz). On any error insertion, the orchestration layer posts an alert to the ops Slack channel (channel ID in 'agent_config' tab B7). The FullSpec team reviews error logs during the first two weeks post-launch as part of hypercare monitoring.
Testing approach
All agents are built and validated in a staging environment first. The staging setup uses a duplicate Google Sheet (named with '-STAGING' suffix), a sandbox Meta Ads account, a GA4 test property, and a HubSpot sandbox portal. Slack messages during testing are posted to a private '#reporting-qa' channel, not the live stakeholder channel. Each agent is tested in isolation before end-to-end runs. End-to-end testing uses two historical reporting periods to validate metric accuracy against known outputs.
Estimated total build time
Data Collection Agent: 9 hrs. Normalisation and Insight Agent: 8 hrs. Distribution Agent: 4 hrs. End-to-end integration testing and QA: 3 hrs. Total: 24 hours.
Before build starts, the following must be confirmed: (1) Meta Business Manager account has API access approved and the correct ad account ID is provided. (2) Google Analytics service account has Viewer access to the correct GA4 property ID. (3) HubSpot Private App token is generated with 'crm.objects.contacts.read' and 'crm.objects.deals.read' scopes. (4) The master Google Sheet ID is confirmed and the 'agent_config' and 'raw_data' tab structure is in place. (5) Looker Studio dashboard is already connected to the master Google Sheet as its data source. (6) The Slack Bot has been installed to the workspace and the target channel ID confirmed. Raise any blockers to support@gofullspec.com immediately so they do not delay the build schedule.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Campaign Performance Reporting.

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