Back to Marketing Attribution 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

Marketing Attribution Reporting

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

This document gives the FullSpec build team everything needed to construct, connect, and ship the Marketing Attribution Reporting automation end to end. It covers the current-state step map, full agent specifications with IO contracts, the end-to-end data flow trace, and the recommended build stack. Nothing in this document requires action from the business owner; it is written for the engineer or technical lead executing the build.

01Process snapshot

Step
Name
Description
01
Pull Google Ads Performance Export
Marketing Manager logs into Google Ads, selects the date range, and downloads a CSV of campaign, ad group, and keyword performance. (15 min)
02
Pull Meta Ads Performance Export
Marketing Manager repeats the export process inside Meta Ads Manager, selecting matching date ranges and relevant columns. (15 min)
03
Pull GA4 Conversion and Source Data
Team exports a source/medium conversion report from Google Analytics 4 to capture organic, direct, and referral traffic conversions. (20 min)
04
Pull HubSpot Deal and Lead Source Data
Contact or deal report exported from HubSpot showing original source, lifecycle stage, and revenue attached to closed deals. (20 min)
05
Reconcile UTM Tags and Fix Discrepancies
Analyst reviews UTM parameters across all exports, manually correcting missing or malformed tags so sources align across platforms. BOTTLENECK. (60 min)
06
Merge All Data Into Master Spreadsheet
All exported files are copied or VLOOKUP-ed into a single Google Sheet using a pre-built template, matching campaigns and date ranges. BOTTLENECK. (45 min)
07
Apply Attribution Model and Calculate Metrics
Analyst applies first-touch, last-touch, or linear attribution model manually and calculates CPA, ROAS, and contribution by channel. (40 min)
08
Cross-Check Totals Against Platform Dashboards
Team cross-references key spend and conversion totals against live platform dashboards to catch import errors. (30 min)
09
Build Summary Slide or Narrative
Short written or slide summary drafted to accompany the data, highlighting channel performance, anomalies, and budget recommendations. (40 min)
10
Distribute Report to Stakeholders via Slack
Final spreadsheet and summary shared via Slack to team members and leadership. (10 min)
Time cost summary: Total manual time per cycle is 295 minutes (approximately 4 hours 55 minutes). At 4 to 6 cycles per month this equals 20 to 30 hours per month, or roughly 6 hours per week across the team. Steps 1, 2, 3, 4, 5, 6, 7, and 10 are replaced by automation. Steps 8 (cross-check totals) and 9 (strategic narrative) remain as deliberate human steps. The two bottleneck steps, 5 and 6, account for 105 minutes per cycle and are the primary source of data errors.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Data Fetch Agent

Connects to Google Ads, Meta Ads Manager, Google Analytics 4, and HubSpot on a fixed schedule or on detection of a campaign end event. Handles OAuth token refresh, API pagination, rate-limit back-off, and date range alignment across all four platforms before writing structured raw data tables to the staging Google Sheet. This is the entry point of the entire automation; no downstream agent fires until this agent confirms a successful four-source write.

Trigger
Scheduled cron (weekly: Monday 06:00 local; monthly: 1st of month 06:00 local) OR campaign end event webhook from Google Ads or Meta Ads Manager.
Tools
Google Ads API (v14+), Meta Marketing API (v18+), Google Analytics Data API (GA4), HubSpot CRM API (v3)
Replaces steps
Steps 1, 2, 3, 4
Estimated build
16 hours — Complex
// Input
report_period: { start_date: 'YYYY-MM-DD', end_date: 'YYYY-MM-DD' }
platforms: ['google_ads', 'meta_ads', 'ga4', 'hubspot']
staging_sheet_id: '<GOOGLE_SHEETS_ID>'
staging_tab_prefix: 'raw_'

// Output (on success)
fetch_status: { google_ads: 'ok'|'error', meta_ads: 'ok'|'error', ga4: 'ok'|'error', hubspot: 'ok'|'error' }
rows_written: { google_ads: <int>, meta_ads: <int>, ga4: <int>, hubspot: <int> }
staging_tabs: ['raw_google_ads', 'raw_meta_ads', 'raw_ga4', 'raw_hubspot']
handoff_signal: 'DATA_FETCH_COMPLETE' | 'DATA_FETCH_PARTIAL' | 'DATA_FETCH_FAILED'

// On error
error_payload: { platform: string, error_code: int, message: string, retry_count: int }
alert_channel: '#marketing-ops-alerts'
  • Google Ads: requires OAuth 2.0 with scope https://www.googleapis.com/auth/adwords. Developer token must be approved for basic access tier minimum. Use customer_id from the linked MCC account. Fields required: campaign.name, campaign.id, metrics.clicks, metrics.impressions, metrics.cost_micros, metrics.conversions, segments.date.
  • Meta Ads Manager: requires Marketing API access; app must be approved for ads_read and ads_management permissions. Use act_<AD_ACCOUNT_ID> as the account identifier. Fields: campaign_name, adset_name, spend, impressions, clicks, actions (purchase event), date_start, date_stop. Pagination via cursor with limit 500.
  • GA4: uses the Google Analytics Data API (not the Universal Analytics Reporting API). Dimensions: sessionDefaultChannelGroup, sessionSource, sessionMedium. Metrics: sessions, conversions, totalRevenue. Date range must match the ad platform period exactly.
  • HubSpot: use the CRM Deals API v3. Filter by closedate within the report period and hs_deal_stage = closedwon. Properties to retrieve: dealname, amount, hs_analytics_source, hs_analytics_source_data_1, createdate, closedate.
  • If any single platform returns an error after three retries, set handoff_signal to DATA_FETCH_PARTIAL and log to the error table. Do not block the downstream agent if three of four sources succeed; proceed with a missing-data flag.
  • Dedupe: before writing to the staging tab, check for an existing row with the same date range and platform. If found, overwrite rather than append.
  • Confirm before build: Google Ads developer token approval status, Meta app review status, HubSpot API key tier (Private App token required, not legacy API key), and GA4 property ID.
Attribution and Merge Agent

Reads all four raw staging tabs written by the Data Fetch Agent, normalises UTM and source labels to a canonical taxonomy, merges the data into the master attribution tab in Google Sheets, and applies the configured attribution model to produce per-channel CPA and ROAS figures. This agent owns all formula logic and must be built against an agreed attribution model before deployment.

Trigger
handoff_signal == 'DATA_FETCH_COMPLETE' OR 'DATA_FETCH_PARTIAL' from Data Fetch Agent (event-driven, not scheduled independently).
Tools
Google Sheets API (v4)
Replaces steps
Steps 5, 6, 7
Estimated build
12 hours — Moderate
// Input
staging_sheet_id: '<GOOGLE_SHEETS_ID>'
staging_tabs: ['raw_google_ads', 'raw_meta_ads', 'raw_ga4', 'raw_hubspot']
attribution_model: 'last_touch' | 'first_touch' | 'linear'  // must be confirmed pre-build
utm_normalisation_map: { 'cpc': 'paid_search', 'paidsocial': 'paid_social', ... }
master_tab: 'attribution_output'

// Output
attribution_table: [
  { channel: string, spend: float, impressions: int, clicks: int,
    conversions: int, revenue: float, cpa: float, roas: float,
    attributed_weight: float, period: 'YYYY-MM-DD/YYYY-MM-DD' }
]
anomaly_flags: [{ channel: string, metric: string, value: float, threshold: float, direction: 'above'|'below' }]
merge_status: 'MERGE_COMPLETE' | 'MERGE_PARTIAL'
missing_channels: [string]  // populated when DATA_FETCH_PARTIAL was received
  • UTM normalisation rules must be documented and agreed with the marketing team before build. At minimum, map: google / cpc to paid_search, facebook / cpc and instagram / cpc to paid_social, (not set) and (direct) to direct. Store the map as a config table in a dedicated sheet tab named utm_config so it can be updated without code changes.
  • Attribution model logic is implemented as calculated columns in the master sheet written by the agent via the Sheets API batchUpdate. Do not hard-code the model; read the attribution_model value from a config cell (sheet: config, cell: B2) at runtime.
  • CPA formula: spend / conversions. ROAS formula: revenue / spend. Both must be calculated per channel row and totalled. Guard against divide-by-zero with an explicit zero-conversion check.
  • Anomaly detection thresholds: flag if ROAS drops more than 30% week-over-week, if spend increases more than 50% versus the prior equivalent period, or if a channel that was present in the previous report is missing entirely. Store threshold values in the config tab.
  • If merge_status is MERGE_PARTIAL, write a visible warning cell in red to the attribution_output tab header row and include missing_channels in the anomaly_flags payload.
  • Confirm before build: the agreed attribution model, the UTM normalisation map reviewed against recent campaign naming, and the named ranges in the master sheet (do not rely on row numbers, use named ranges).
Summary and Delivery Agent

Reads the completed attribution table from the master Google Sheet, identifies the top-performing channel by ROAS, surfaces any anomaly flags raised by the Attribution and Merge Agent, and writes a short plain-language summary. Posts the Google Sheet link and the summary text to the designated Slack channel, tagging the marketing manager for review. This agent fires only after merge_status is confirmed.

Trigger
merge_status == 'MERGE_COMPLETE' OR 'MERGE_PARTIAL' from Attribution and Merge Agent.
Tools
Google Sheets API (v4), Slack API (Web API, chat.postMessage)
Replaces steps
Step 10
Estimated build
6 hours — Simple
// Input
attribution_table: [ { channel, spend, cpa, roas, conversions, revenue } ]
anomaly_flags: [ { channel, metric, value, threshold, direction } ]
merge_status: 'MERGE_COMPLETE' | 'MERGE_PARTIAL'
sheet_url: '<PUBLIC_OR_SHARED_GOOGLE_SHEET_URL>'
slack_channel_id: '<CHANNEL_ID>'  // e.g. C0123456789
mention_user_id: '<SLACK_USER_ID>'  // Marketing Manager

// Output
slack_message: {
  text: string,  // plain-language summary, max 500 characters
  blocks: [ section_block, context_block ]  // Block Kit layout
  attachments: [ { title: 'View Attribution Report', url: sheet_url } ]
}
delivery_status: 'DELIVERED' | 'SLACK_ERROR'

// On MERGE_PARTIAL
summary_includes_warning: true
missing_channels_listed: [string]  // named in the Slack message body
  • Slack integration requires a Bot Token (xoxb-) with scopes: chat:write, chat:write.public. Store the token in the shared credential store, not in the workflow configuration directly.
  • The summary text must be generated from the attribution_table data, not hard-coded. Minimum required sentences: top channel by ROAS, total spend for the period, total conversions, and any anomaly flags. If anomaly_flags is empty, state that all channels performed within expected thresholds.
  • Use Slack Block Kit for message formatting: one section block for the summary paragraph, one context block listing the report period and the merge_status value, and one actions block with the sheet link button.
  • If delivery_status is SLACK_ERROR, write the error to the Supabase error log table and send a fallback plain-text message to the #marketing-ops-alerts channel.
  • Do not tag the marketing manager user ID unless merge_status is MERGE_COMPLETE; on MERGE_PARTIAL, tag the user and include the missing channels explicitly so they can investigate before using the report.
  • Confirm before build: the Slack channel ID and marketing manager Slack user ID, and whether the Google Sheet should be shared as view-only or with comment access for all recipients.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full trigger-to-output data flow for Marketing Attribution Reporting. Field names match the agent IO contracts above.
// ── TRIGGER ─────────────────────────────────────────────────────────────────
SCHEDULE_TRIGGER fires (cron: weekly Mon 06:00 | monthly 1st 06:00)
  OR campaign_end_event received from Google Ads / Meta webhook

payload_in: {
  report_period: { start_date, end_date },
  platforms: ['google_ads', 'meta_ads', 'ga4', 'hubspot'],
  staging_sheet_id,
  staging_tab_prefix: 'raw_'
}

// ── AGENT 1: DATA FETCH AGENT ────────────────────────────────────────────
// Step A: Google Ads API call
GoogleAds.query(GAQL) ->
  { campaign.name, campaign.id, metrics.clicks, metrics.impressions,
    metrics.cost_micros, metrics.conversions, segments.date }
  -> write to staging_tab: 'raw_google_ads'

// Step B: Meta Marketing API call
MetaAds.get('/act_{AD_ACCOUNT_ID}/insights') ->
  { campaign_name, adset_name, spend, impressions, clicks,
    actions[purchase], date_start, date_stop }
  -> write to staging_tab: 'raw_meta_ads'

// Step C: GA4 Data API call
GA4.runReport({ dimensions: [sessionDefaultChannelGroup, sessionSource,
                sessionMedium], metrics: [sessions, conversions,
                totalRevenue], dateRange: { start_date, end_date } }) ->
  { channel_group, source, medium, sessions, conversions, revenue }
  -> write to staging_tab: 'raw_ga4'

// Step D: HubSpot Deals API call
HubSpot.crm.deals.search({ filter: closedate IN period, stage: closedwon }) ->
  { dealname, amount, hs_analytics_source, hs_analytics_source_data_1,
    createdate, closedate }
  -> write to staging_tab: 'raw_hubspot'

// Agent 1 emits handoff signal
handoff_signal: 'DATA_FETCH_COMPLETE' | 'DATA_FETCH_PARTIAL' | 'DATA_FETCH_FAILED'
fetch_status: { google_ads, meta_ads, ga4, hubspot } -> each: 'ok'|'error'
rows_written: { google_ads: <int>, meta_ads: <int>, ga4: <int>, hubspot: <int> }

// ── AGENT 2: ATTRIBUTION AND MERGE AGENT ────────────────────────────────
// Receives: handoff_signal IN ['DATA_FETCH_COMPLETE', 'DATA_FETCH_PARTIAL']

// Step E: Read all raw tabs
GoogleSheets.read('raw_google_ads') -> raw_google_ads_rows[]
GoogleSheets.read('raw_meta_ads')   -> raw_meta_ads_rows[]
GoogleSheets.read('raw_ga4')        -> raw_ga4_rows[]
GoogleSheets.read('raw_hubspot')    -> raw_hubspot_rows[]

// Step F: UTM normalisation
GoogleSheets.read('utm_config') -> utm_normalisation_map
FOR EACH row IN all_raw_rows:
  row.channel_normalised = utm_normalisation_map[row.source + '/' + row.medium]
    | 'other'  // fallback if no match found

// Step G: Merge and attribute
attribution_model = GoogleSheets.readCell('config', 'B2')
  // 'last_touch' | 'first_touch' | 'linear'

FOR EACH channel_normalised IN unique_channels:
  attributed_weight = applyModel(attribution_model, channel_touchpoints)
  spend  = SUM(google_ads.cost_micros/1e6) + SUM(meta_ads.spend) for channel
  conversions = SUM(ga4.conversions) * attributed_weight
  revenue = SUM(hubspot.amount) * attributed_weight
  cpa    = spend / conversions  // 0-guard applied
  roas   = revenue / spend      // 0-guard applied

  -> write row to master_tab: 'attribution_output'
     { channel_normalised, spend, impressions, clicks, conversions,
       revenue, cpa, roas, attributed_weight, period }

// Step H: Anomaly detection
FOR EACH channel IN attribution_output:
  IF roas_delta_wow < -0.30 -> anomaly_flags.push({ channel, 'roas', ... })
  IF spend_delta_wow > 0.50  -> anomaly_flags.push({ channel, 'spend', ... })
  IF channel MISSING vs prior_report -> anomaly_flags.push({ channel, 'missing', ... })

merge_status: 'MERGE_COMPLETE' | 'MERGE_PARTIAL'
missing_channels: [string]

// ── AGENT 3: SUMMARY AND DELIVERY AGENT ─────────────────────────────────
// Receives: merge_status IN ['MERGE_COMPLETE', 'MERGE_PARTIAL']

// Step I: Read attribution output
GoogleSheets.read('attribution_output') -> attribution_table[]
top_channel = MAX(attribution_table, key='roas').channel_normalised

// Step J: Build Slack message
summary_text = buildSummary({
  top_channel, total_spend, total_conversions, total_revenue,
  anomaly_flags, merge_status, missing_channels
})

slack_message = {
  text: summary_text,
  blocks: [ section(summary_text), context(period, merge_status) ],
  attachments: [{ title: 'View Attribution Report', url: sheet_url }]
}

// Step K: Post to Slack
Slack.chat.postMessage({
  channel: slack_channel_id,
  ...slack_message,
  mention: mention_user_id  // only on MERGE_COMPLETE or MERGE_PARTIAL
})

delivery_status: 'DELIVERED' | 'SLACK_ERROR'

// ── END OF FLOW ───────────────────────────────────────────────────────────
// On any SLACK_ERROR: write to Supabase error_log, post to #marketing-ops-alerts
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation tool with native HTTP/OAuth nodes and a credential store. One workflow per agent (three workflows total): 'data-fetch', 'attribution-merge', and 'summary-delivery'. Each workflow is triggered by the completion event of the prior workflow, not by a shared cron, to prevent race conditions. All three share a single credential store for OAuth tokens and API keys. Do not couple the workflows with direct function calls; use a lightweight status record (see error logging below) as the inter-workflow handoff mechanism.
Webhook configuration
One inbound webhook endpoint is required for the optional campaign-end trigger from Google Ads and Meta. The endpoint should accept a POST payload containing { platform, campaign_id, end_date } and immediately queue a Data Fetch Agent run for the relevant period. Secure with a shared secret header (X-Fullspec-Secret) validated before any processing begins. The scheduled cron trigger remains the primary path; the webhook is additive.
Templating approach
The Slack summary message is built from a plain-text template string stored in the automation platform's environment variables. Placeholders use double-brace syntax: {{top_channel}}, {{total_spend}}, {{total_conversions}}, {{anomaly_count}}. The UTM normalisation map and attribution model selection are stored as a dedicated config tab in the master Google Sheet (tab name: config) so the marketing team can update them without touching the workflow. Attribution model is read from cell B2 of that tab at runtime on every merge run.
Error logging
A Supabase table named automation_error_log captures all agent-level failures. Schema: id (uuid), workflow_name (text), step_name (text), error_code (int), error_message (text), platform (text), created_at (timestamptz), resolved_at (timestamptz nullable). On any error, the agent writes a row to this table and posts a plain-text alert to the Slack channel #marketing-ops-alerts with the workflow name, failing step, and error message. Supabase is the single source of truth for error history. Alerts in Slack are for immediate visibility only.
Testing approach
All three agents must be built and tested in a sandbox environment before any production API credentials are used. The sandbox uses a duplicate Google Sheet (prefixed 'sandbox_'), a separate Slack channel (#reporting-sandbox), and test API credentials where available (Google Ads sandbox, Meta test ad accounts). Each agent is tested in isolation before end-to-end testing begins. End-to-end testing runs the full flow against a historical reporting period so output can be validated against the known manual report for that period.
Estimated total build time
Data Fetch Agent: 16 hours. Attribution and Merge Agent: 12 hours. Summary and Delivery Agent: 6 hours. End-to-end integration testing and QA: 4 hours. Total: 38 hours across 4 build weeks as scoped.
API access timelines are the single biggest build risk. Google Ads developer token approval and Meta Ads app review can each take two to five business days and cannot be parallelised with build work that depends on them. Begin the access request process before the build start date. If access is delayed, the Attribution and Merge Agent can be built and tested against static fixture data in the interim, but the Data Fetch Agent cannot be fully validated until live API credentials are available.
Before build starts, confirm the following with the business owner: (1) the agreed attribution model, first-touch, last-touch, or linear; (2) the HubSpot Private App token has been created with crm.objects.deals.read scope; (3) the GA4 property ID is known and the service account has Viewer access; (4) the Slack channel ID and marketing manager user ID for tagging; and (5) the master Google Sheet has been shared with the automation service account with Editor permissions. None of these can be resolved during build. Reach the FullSpec team at support@gofullspec.com with any access issues.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Marketing Attribution 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