Connecting advertising data to a CRM is useful only if the combined report preserves what each row means. Campaign spend belongs to a campaign and reporting period. A lead belongs to a person or company. Copying the first onto every instance of the second can multiply your costs without anyone spending another dollar.
For an Attio workflow, define those two datasets before selecting the connector.
Keep campaign facts separate
Aggregate campaign costs and CRM outcomes separately, then join once on stable campaign identifiers. Count unmatched opportunity records separately so the report makes missing attribution visible.
Use a campaign performance table with a stable key: platform, advertising account ID, campaign ID and reporting date. Include currency, spend and the platform-reported metrics you actually need. Store campaign names as labels rather than identifiers.
A sync should update the existing row for the same key. Appending another copy each time it runs will inflate a report. Preserve the retrieval timestamp so you can tell when a value was refreshed.
If you combine currencies, define a conversion policy before adding the amounts. Otherwise, keep them separate.
Preserve acquisition context in the CRM
On the CRM side, retain the acquisition identifiers you actually captured, the associated person or company, the opportunity relationship and relevant stage dates. Distinguish an original acquisition source from a later interaction instead of repeatedly overwriting a single source field.
A campaign name alone is a fragile matching mechanism. Names can change and different advertising accounts can reuse them. Match with a stable captured identifier where available, and leave unmatched records visible rather than guessing their origin.
These fields describe your chosen attribution rule. They do not prove that an advertisement caused a purchase.
Avoid the multiplied-spend problem
Consider a synthetic example. Campaign A spends $500 in a reporting period and produces ten associated contacts. If each contact receives a $500 campaign-spend field, a sum across contacts reports $5,000.
Instead, aggregate opportunity outcomes to the campaign and period you intend to measure. Join that aggregate with the campaign cost table once. If there are two qualified opportunities in the defined cohort, the simple cost-per-qualified-opportunity calculation is $500 divided by two, or $250.
The arithmetic is straightforward; the cohort definition is not. State whether the opportunities were acquired during that period or merely qualified during it. Those are different questions, especially when the sales cycle spans several weeks.
Do not add Google and Meta platform-attributed conversions and assume the result is a deduplicated customer total. Keep platform reporting and your CRM opportunity counts labeled separately.
Test the pipeline before expanding it
Begin with one campaign and a handful of synthetic or appropriately permissioned CRM records. Run these checks:
- Run the same sync twice. Spend and record counts should not double.
- Rename the campaign. The stable identifier should still connect the same records.
- Advance an opportunity to a new stage. Preserve the stage history needed for your reporting question.
- Remove a usable acquisition identifier from a test record. It should become unmatched rather than attach to an arbitrary campaign.
- Refresh a previously imported date. Confirm the row updates according to your intended policy.
Keep a small reconciliation report outside the CRM while testing. It should show source totals, imported totals and unmatched records. Push only the fields that help the sales team or reporting workflow into Attio.
Choose the connector after the model
Compare connectors on the exact reads, destination writes, refresh behavior and error reporting you need. “Supports Google Ads” does not establish support for every field or operation. Confirm how credentials and client workspaces are scoped before using the workflow across an agency portfolio.
The practical goal is a report that joins campaign economics with qualified pipeline without inventing identity matches or duplicating cost. Once that works on a small sample, broader automation becomes easier to evaluate.
Reproduce the join with synthetic data
The companion SQL file runs in SQLite without external services. Download it and run sqlite3 :memory: < attio-campaign-example.sql. It deliberately uses one account, one campaign and one currency. Two daily cost rows total $500. Three matched opportunities were acquired between September 1 and September 23; two qualified by September 23. A fourth opportunity has no captured campaign identifier and stays in a separate unmatched count.
| Campaign | Spend | Matched opportunities | Qualified by cutoff | Cost per qualified opportunity |
|---|---|---|---|---|
| campaign-a | $500 | 3 | 2 | $250 |
The query aggregates costs and outcomes independently before joining them. Replaying the September 1 cost row updates its value instead of adding a duplicate. If there are no qualified opportunities, cost per qualified opportunity is NULL rather than zero. Those behaviors were checked locally on September 23, 2026.
This example uses a single captured acquisition campaign per opportunity. A multi-touch model requires explicit allocation rules. Insert and update guards enforce one currency per campaign. Normalize mixed-currency sources under a documented policy before adapting the example. The reporting dates are defined once in reporting_window. To adapt this to Attio, map exported records to the sample opportunity schema. The example does not connect to Attio or establish API compatibility.
Before connecting an ads agent to this workflow, run the read-only Google Ads acceptance test to reconcile source metrics.