I started with a standalone dashboard that matched outbound campaigns to deals and let a human approve the credit. It proved the value quickly. Then the whole company moved from last touch to a linear multi-touch model, which meant every EmailBison send and Lemlist sequence or LinkedIn touch had to become structured history in HubSpot.
Attributing GTM engineering work is difficult for a boring reason: the work happens across too many systems. A workflow finds the account, another tool enriches the contact, EmailBison sends the cold email, Lemlist runs a sequence, and HubSpot eventually records the meeting and deal. Apollo held the roster, EmailBison held cold email activity, Lemlist held sequenced outreach, and HubSpot held the deal, and none of them talked to each other. Every quarterly review, "did outbound source this?" got answered by memory and a manual export, and the number changed depending on who was asked.
My first answer was an MVP attribution dashboard: one place that pulled the outbound tools and CRM together, matched campaign recipients to deal contacts by email address, and put every possible match in a review queue rather than a live report. Nothing counted until a human approved it, and approved historical numbers stayed fixed. What the reviewer actually saw:
| Field | What it showed |
|---|---|
| Contact + company | Who was touched, matched to the HubSpot record by email address. |
| Source campaign | Which EmailBison, Lemlist, or Apollo-sourced campaign the touch came from, with its name and timestamp. |
| Matched deal | The HubSpot deal the contact was associated with, including stage and amount. |
| AI-drafted rationale | A short generated explanation of why the match looked plausible, written for the reviewer, not treated as the final answer. |
| Gong call evidence | Transcript search for "how did you hear about us" style answers, to catch touches an email match alone would miss. |
| Approve / deny | A human decision on every single match. Nothing counted in a dashboard number until approved. |
Email matching alone missed a specific, common pattern: someone gets the cold email, never replies to it, then books a meeting inbound weeks later because their boss forwarded it to them and said to check it out. The dashboard pulled Gong call transcripts and searched them for "how did you hear about us" answers, so a rep saying that on a call counted as evidence too, not just a reply in the inbox. Linear multi-touch doesn't have a clean way to credit that kind of secondhand touch, but when there's no attribution system at all yet, catching it manually beats missing it entirely.
flowchart LR TOOLS[Apollo + EmailBison + Lemlist
campaign recipients] --> MATCH[Match recipient email
to HubSpot deal contacts] CRM[HubSpot
deals + contacts] --> MATCH MATCH --> AI[AI drafts a rationale
for the reviewer] AI --> QUEUE[Review queue] QUEUE -->|approve| COUNT[Counts in the
attribution number] QUEUE -->|deny| EXCLUDE[Excluded] COUNT --> LOCK[Historical result
stays locked] style QUEUE stroke:#10b981,stroke-width:2px style COUNT stroke:#8b5cf6
It was intentionally not a perfect company-wide attribution model, and it was never going to be the permanent answer. But when I built it, scale wasn't the problem: most cold email programs have no idea if they're working, and "is this working?" gets answered with a shrug. Before anyone could talk about multi-touch or linear credit, leadership needed one honest yes or no — are people booking off the emails we're sending, and is it turning into pipeline? The MVP existed to prove that, with receipts, and it did.
The next stage was bigger than my dashboard. The whole company moved from last-touch reporting to a linear multi-touch model, splitting credit across every recorded touch on the journey instead of handing it all to whatever happened last. The catch: it can only split credit among events HubSpot actually has. Smartlead, EmailBison, and most outbound platforms run their campaigns outside the CRM, and Lemlist's LinkedIn touches — connection requests, messages, replies — are the hardest of all to preserve unless the platform pushes them back to the contact timeline itself.
OutboundSync became the connective layer. When EmailBison or Lemlist produced an event, a webhook sent it to OutboundSync. OutboundSync normalized the different payloads, matched the person to HubSpot by email address, and wrote the activity onto the contact timeline with its campaign metadata.
EmailBison supplied email sends, replies, automated replies, opens, bounces, unsubscribes, interest signals, tags, campaign IDs, names, and timestamps. Lemlist supplied email activity plus the LinkedIn sequence touches that were otherwise hardest to preserve. HubSpot received one consistent event history instead of two incompatible exports.
flowchart LR
subgraph SEP["Where outreach happened"]
EB[EmailBison
cold email]
LL[Lemlist
email + LinkedIn sequences]
end
EB -->|webhook events| OS[OutboundSync
normalize + match]
LL -->|webhook events| OS
OS -->|match by email| C[HubSpot contact]
OS -->|timeline activities| C
OS -->|campaign properties| C
C --> CO[Associated company]
C --> D[Associated deal]
C --> WF[Lists + workflows]
D --> ATTR[Company-wide linear
multi-touch model]
style OS stroke:#8b5cf6,stroke-width:2px
style ATTR stroke:#10b981,stroke-width:2px
| Layer | What it became responsible for |
|---|---|
| EmailBison | Operational truth for cold email: campaign, sender, send, reply, bounce, unsubscribe, and reply classification. |
| Lemlist | Operational truth for sequenced outreach, including the LinkedIn connection requests, messages, and replies that are difficult to attribute otherwise. |
| OutboundSync | Webhook ingestion, event normalization, contact matching, activity creation, and campaign property updates. |
| HubSpot | The shared timeline connecting outbound activity to contacts, companies, meetings, deals, workflows, and the company attribution model. |
The synced properties made this operational. Fields such as last email campaign name, last sent time, and last reply time made outbound contacts available to HubSpot lists, workflows, and reports. Campaign naming had to be exact before launch because the attribution chain used that name to connect the synced activity back to the campaign. Renaming a live campaign could orphan the historical record.
flowchart LR EMAIL[EmailBison
cold email] --> LINKEDIN[Lemlist
LinkedIn touch] LINKEDIN --> SEARCH[Branded search
+ content] SEARCH --> FORM[Demo form
+ meeting] FORM --> DEAL[Deal] DEAL --> LAST[Old last-touch model:
form gets 100%] DEAL --> LINEAR[Linear multi-touch:
each recorded touch
shares credit equally] style LAST stroke:#f59e0b style LINEAR stroke:#10b981,stroke-width:2px
Linear attribution doesn't prove every touch was equally causal — no model does that on its own. What changed is that outbound finally sits inside the same measurement system as the rest of the company, with complete evidence instead of a story assembled after the fact.
Implementation details were validated against how EmailBison and Lemlist actually fire these events, plus the official HubSpot attribution documentation.