Skip to content
Top 1% Upwork (8 years) 286+ client deployments 2,036+ projects shipped GoHighLevel Certified Partner Featured speaker: GHL Summit 2025 Client Login
← All issues
The Scale Brief · Issue #151

Your lead remembers where it came from.
Your CRM has already forgotten.

Click your own ad. Land on your funnel. Submit the form. Open your CRM. Look at the contact you just made. Do you see which ad? Most teams don't. The lead has been to five hands by then. The source got dropped somewhere between hand two and hand five.

This is the most common conversation I have in client kickoffs. The CMO is convinced a specific ad set is killing it. The CFO can't see the attribution and won't increase the budget. The agency in the middle is showing impressions and clicks, but no one can draw a clean line from that ad to this booked call to this signed contract.

The line exists. It got cut. Usually in the same place. Often in three places.

The five-link stitch

Every lead in your CRM should carry where it came from on its back. End to end, the chain is five hops:

If the chain is intact, you can look at a contact in your CRM and read back the original ad copy. If one link is missing, attribution collapses into "direct" or "organic" — the categories you can't optimize against.

Where the chain breaks

Three places. Almost always.

Break 1: Landing page strips the params

Marketer tags the ad URL with ?utm_source=fb-retargeting-warm&utm_campaign=q2-coaches. The landing page is a one-pager built with whatever template their site is on. The CTA button is hard-coded as <a href="/apply">. The user clicks. The query params evaporate. Done.

// What most templates ship
<a href="/apply" class="btn-primary">Apply now</a>

// What you actually need
<a href="/apply?source=fb-retargeting-warm-q2-coaches" class="btn-primary">Apply now</a>

You can't hand-write that for every page. The pattern is to read the inbound query params on page load and inject them into every internal CTA:

// One line on every landing page. Reads inbound utm_source/utm_campaign,
// appends to every internal anchor that points to a conversion endpoint.
function stitchSource() {
  const inbound = new URLSearchParams(location.search);
  const source = inbound.get('utm_source')
              || inbound.get('source')
              || inferFromReferrer(document.referrer);
  if (!source) return;
  document.querySelectorAll('a[href^="/apply"],a[href^="/book"]').forEach(a => {
    const u = new URL(a.href, location.origin);
    if (!u.searchParams.has('source')) u.searchParams.set('source', source);
    a.href = u.toString();
  });
}
stitchSource();

Now every internal CTA carries the ad's source forward. Even if the visitor hops two pages before they click.

Break 2: The form forgets the page it lives on

The CTA hands the visitor to /apply?source=fb-retargeting-warm-q2-coaches. They fill the form. The form posts { name, email, phone, company } to the lead endpoint. Source missing.

Every form on the site needs to read the URL params on page load and inject them as hidden fields the user can't see but the server gets:

// Inside the form, on page load — adds 4 hidden fields per submit
function attachProvenance(form) {
  const url = new URL(location.href);
  const out = {};
  url.searchParams.forEach((v, k) => { out[k] = v; });
  out.landing_page = location.pathname;                       // the page itself
  out.referrer     = document.referrer || 'direct';           // where they came from
  out.submitted_at = new Date().toISOString();                // when
  for (const [k, v] of Object.entries(out)) {
    const input = document.createElement('input');
    input.type = 'hidden'; input.name = k; input.value = v;
    form.appendChild(input);
  }
}

Now the lead payload that hits your endpoint carries the source, the landing page it converted on, the referrer, and a timestamp. Four fields. Worth it.

Break 3: The CRM stores the contact but not the tag

The endpoint receives { email, source: 'fb-retargeting-warm-q2-coaches', landing_page: '/apply', referrer: '...' }. It calls the CRM API. It says "create a contact named Adam Palmer with email [email protected]." The contact gets created. The source went into a note that nobody reads.

The CRM-level fix is to tag the contact with the source at create time, not attach it as freeform text:

// Tags drive workflows. Notes don't. Always create with tags.
async function upsertContact(payload) {
  const tags = [
    'as-website',
    `form-${payload.form_type}`,
    `src-${normalize(payload.source)}`,  // ← THIS LINE
  ];
  return await ghl.contacts.upsert({
    email: payload.email,
    firstName: payload.name?.split(' ')[0],
    lastName:  payload.name?.split(' ').slice(1).join(' '),
    phone:     payload.phone,
    source:    payload.source,           // also store as native field
    tags,                                // and tag-driven for workflows
  });
}
function normalize(s) {
  return String(s||'direct').replace(/[^a-z0-9-]/gi, '-').toLowerCase().slice(0, 64);
}

Now the contact has a tag like src-fb-retargeting-warm-q2-coaches. That tag can fire a workflow specifically for warm Facebook retargeting from the Q2 coaches campaign. The CRM has finally caught up to what the ad was.

The audit signal

You don't have to deploy the whole stitch to know whether yours is intact. Three minutes of click-tracing tells you.

  1. Open a private browser window. Pick the most expensive paid campaign you're running. Click your own ad.
  2. Click any internal link before you reach a form. Two pages forward.
  3. Open the form. View the HTML. Search for the value of your utm_source. Is it there as a hidden field?
  4. Submit a dummy lead. Open it in the CRM. Is there a tag containing the source?

If any of those four steps fails, your stitch is cut. The next time the CFO asks "is the Q2 coaches campaign working," your answer is going to be a shrug.

What you can do today

The stitch isn't a project. It's three patches.

  • Patch your CTAs — drop the stitchSource() snippet into a shared script. Loads on every page. Every internal link now carries the source forward.
  • Patch your formsattachProvenance() on form load. Every submission now carries source + landing page + referrer + timestamp.
  • Patch your CRM creation — tag at upsert. Workflows fire off tags. Notes don't.

None of these need a vendor. None need a budget. Total ship time is one afternoon for one developer. The result is that every contact in your CRM is readable as a story instead of an anonymous row.

The audit

At AutomateScale, every client's first month includes a lead-stitch audit. We click through every paid channel, every organic landing, every funnel — and trace the source all the way to the GHL tag. Then we patch the cuts. The result is usually 20-40% of historical leads recategorized from "direct" or "organic" into the actual campaign that produced them. Want the audit on your funnel? Apply for an audit.

The one-line summary

Attribution doesn't survive on its own. Five hops separate the ad from the booked call: ad, landing, CTA, form, CRM. The stitch is three lines of code at three of those hops. Without the stitch, you cannot optimize what you cannot trace. With it, every contact in your CRM is a readable story — and the next time someone asks which campaign is working, the answer is on the contact itself.

★ Get the audit
A free 30-minute call. We screen-share your live funnel and trace the stitch on three campaigns in real time.
Apply for the audit
Stitch every lead back to where it came from

The Scale Audit traces every lead in your CRM
back to the ad that produced it.

Three campaigns, traced live, sources patched end-to-end. The leads you thought were "organic" usually weren't.

Apply for a free audit All issues