Franchise sales can take months. By the time a candidate signs, the team may no longer remember how they arrived. If the CRM holds one editable “Lead source” field that someone typed in once, channel decisions rest on a guess.

This guide is a design tutorial. It shows a proposed setup: tagged links, three layers of fields with one meaning each, capture and persistence rules, and field-level expected results for four fictional journeys. It is not a record of a tested SMF implementation. Real behavior has to be confirmed by running the tests at the end on your own stack.

Every link you place outside your site (social posts, emails, video descriptions, partner pages, manual ad links) carries UTM parameters:

https://example.com/own-a-franchise/?utm_source=linkedin&utm_medium=social&utm_campaign=owner_role_us_q2&utm_content=founder_post_03
ParameterValueRule
utm_sourcelinkedinThe platform. Lower case, always the same spelling
utm_mediumsocialUnpaid. Paid placements use a separate value, such as paid_social
utm_campaignowner_role_us_q2The initiative. Stable for the campaign’s life
utm_contentfounder_post_03The specific post or creative

Google Analytics treats UTM values as case-sensitive, so “LinkedIn” and “linkedin” become two sources [S1]. Keep a register of allowed values.

Google Ads and some other platforms also support auto-tagging, which adds a click ID instead of, or alongside, manual tags. Analytics treats manual and auto-tagged traffic differently [S2]. Decide which one each platform uses and record that decision.

Don’t add UTMs to internal links between your own pages. They can overwrite the visitor’s real campaign data and pollute attribution. Check what your own analytics setup does rather than assuming. Never put names or emails in a URL.

Step 2: three layers, each with one job

One source field can end up trying to do three jobs. Split them:

  1. The browser observation window. What this browser has shown us since storage last started. It’s temporary: it expires, gets cleared, and doesn’t exist on a new device.
  2. The contact history in the CRM. The persistent record of what we have observed about this person, retained under the CRM’s retention and deletion policy. Cookie expiry does not overwrite earlier observations; authorized correction or deletion follows that separate policy.
  3. The inquiry. What was true on the visit when this particular inquiry was submitted.
FieldMeaning
window_idA new ID each time a window starts
window_started_atWhen this window started
window_reset_reasonWhy it started: first_visit, expired, storage_cleared, new_device, consent_changed, or unknown
win_source / win_medium / win_campaign / win_contentSource values of the first visit in this window, including direct (stored as “(direct)”)
win_provenanceHow that source was known: utm, referrer, click_id, none
click_id_type / click_id / click_atA paid click ID (gclid, fbclid) seen in this window, and its landing-observation time
consent_stategranted, denied, unknown

The window expires after a set period, for example 90 days. After that, the next visit starts a new window. Record expired only when an existing timestamp or permitted expiry marker establishes it. Empty storage alone cannot distinguish a first visit, cleared storage, an expired cookie or a new device. Use unknown unless the reason is supported; never infer a device history from missing cookies.

Layer 2: contact history (CRM, append-only)

FieldMeaning
contact_first_observed_source / _medium / _campaign / _content / _at / _provenanceCopied from the window linked at the contact’s first inquiry. Written once, never overwritten
observation_logOne row per window ever linked to this contact: window_id, started_at, win_ fields, reset reason
identity_merge_logOne row per identity decision: which window or record was linked to which contact, the method (for example “same email submitted on form”), who or what decided, and when
contact_first_observed_correction / _reasonA correction kept next to the original, never replacing it

When the same person comes back after their window expired, or on a new device, the new window is added to observation_log once an inquiry or another explicit identification links it to the contact. The contact_first_observed_* fields don’t change. A cookie expiring is never a reason to rewrite a person’s history.

When identity can’t be established (consent denied, no window, no match), record unknown (no linked observation window). Don’t guess, and don’t copy the inquiry source into the first-observed fields to fill the gap.

Layer 3: the inquiry (one record per inquiry)

FieldMeaning
inq_visit_source / _medium / _campaign / _contentSource of the visit on which the form was submitted. A direct return visit is “(direct)”
inq_landing_page / inq_submitted_atPage and time of submission
inq_window_idThe observation window active at submission
carried_click_id_type / carried_click_id / carried_click_atA paid click ID carried from earlier in the same window, with the original click time
self_reported_sourceCandidate’s answer to “How did you hear about us?” plus the rep’s call notes

If several paid clicks occur in one window, append them to a click log and carry the most recently observed eligible click for each platform into the inquiry, with its original observation time. This is the tutorial’s selection policy, not proof of which click caused the signing. The landing observation time is not a separately verified ad-platform click timestamp.

A carried paid click ID doesn’t make the inquiry visit paid. If someone clicks an ad on Monday and comes back directly on Thursday to submit, the inquiry visit is direct. The click ID is carried alongside it, so the conversion can still be sent back to the ad platform. Both facts are recorded. Neither replaces the other.

Step 3: capture and persistence rules

  • Capture on arrival. Read UTMs and click IDs from the landing URL on the first page load. The query string disappears once the visitor navigates.
  • Persist within the window where consent permits, then copy window and carried-click values into hidden form fields at submission.
  • Consent denied. Store only what your policy and configuration allow. Typically that’s the submission visit’s own URL values, with consent_state = denied and no window. An untagged organic visit can’t be identified from the URL alone, so it stays unknown.
  • Click IDs alone don’t send outcomes back. Each platform needs its own offline-conversion or CRM integration.
  • AI assistants and other referrers may pass a referrer, campaign parameters, or nothing. Record what’s present. A mention in an AI answer with no click is invisible here.

Step 4: expected field values for four journeys

These are the values the design should produce. They’re fictional and unobserved until you run the tests in step 6.

J1. Tagged ad arrival, then direct return, then submission (consent granted)

FieldAfter visit 1 (3 Mar 10:02, Meta ad, fbclid present)After visit 2 (6 Mar, direct, submits)
window_id / reset reasonW1 / first_visitW1 (unchanged)
win_source / medium / campaign / provenancefacebook / paid_social / owner_role_us_q2 / utmunchanged
click_id_type / click_id / click_atfbclid / FB-EXAMPLE-1 / 3 Mar 10:02unchanged
inq_visit_source / medium(none yet)(direct) / (none)
carried_click_*(none yet)fbclid / FB-EXAMPLE-1 / 3 Mar 10:02
contact_first_observed_*(no contact yet)facebook / paid_social / owner_role_us_q2, at 3 Mar 10:02, provenance utm

J2. Window expires, the same person returns and inquires again

FieldExpected
New visit 20 Jul, organic searchwindow W2, reset reason expired if supported by a retained expiry marker (otherwise unknown), win_source google / organic, provenance referrer
Second inquiry, 20 Julnew inquiry record: inq_visit_source google / organic; no carried click
Identityemail matches the existing contact; identity_merge_log row: W2 → contact C-EXAMPLE-1, method “same email on form”, 20 Jul
observation_lognow W1 and W2
contact_first_observed_*unchanged (facebook / paid_social, 3 Mar)

J3. Repeat inquiry from a new device, consent denied

FieldExpected
Windownone; consent_state denied
inq_visit_sourcefrom the submission URL if tagged; otherwise unknown
Identityemail match → merge log row; observation_log gets no new window
contact_first_observed_*unchanged

J4. Booking on an external calendar after J1’s inquiry

FieldExpected
Booking recordcarries the inquiry’s submission ID so the CRM can join it
Source fieldsnone created by the booking tool; the inquiry’s fields stay the source of record
Checkthe join is tested separately from any cross-domain analytics linking

Step 5: other edge cases

SituationExpected result
First visit direct, later tagged visit in the same windowwin_source = (direct), provenance none; the tagged values are recorded on the visit, and on the inquiry if that visit submits
Broker introduces an existing contactnew inquiry with inq_visit_source = broker (manual entry); earlier history untouched
Nothing known“unknown”, never defaulted to “direct” or “website”

Track the unknown share monthly. A rise can mean something broke, but it can also come from changes in consent rates, devices or channel mix. Investigate before concluding.

Step 6: test before trusting

Run each case with a marked test record and compare the results with steps 4 and 5. Record what you actually observe in the worksheet’s observed column, not what you expected. Until a test has been run, its observed value is “pending”:

  1. Tagged link, submit on the same page.
  2. Tagged link, browse two pages, then submit.
  3. Tagged ad click today, direct return later, then submit (journey J1). Check the window fields, inq_visit_source = (direct), and carried_click_* with the original click time.
  4. Consent denied, tagged link, then submit.
  5. Direct first visit, then a tagged visit, then submit.
  6. Repeat inquiry from an existing contact after window expiry or on a new device (journeys J2 and J3). contact_first_observed_* must not change.
  7. Tagged link, then the external booking flow (journey J4).

Rerun the tests after any website, form or consent-banner change.

Start here

Pick tests 1 and 3. Run them, open the CRM record, and compare each field with the J1 table. Every mismatch is a fix on your list.

Download: source-tracking-schema.csv: the three-layer field dictionary, UTM register, capture rules, the J1 to J4 expected values, and test cases with an observed column (pending).

Related: Ad leads versus CRM · Cost per signed franchise

Sources (checked by Codex 28 Oct 2026): [S1] Google Analytics: collect campaign data with custom URLs · [S2] Google Analytics: traffic-source dimensions, manual tagging and auto-tagging

Put it to work on your recruitment system

Bring your recruitment page and anonymized stage counts to a diagnostic. We’ll identify what to test and how to measure it.

Discuss your franchisor recruitment system

More franchisor guides · Read the research and limitations