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.
Step 1: tag the links you control
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
| Parameter | Value | Rule |
|---|---|---|
| utm_source | The platform. Lower case, always the same spelling | |
| utm_medium | social | Unpaid. Paid placements use a separate value, such as paid_social |
| utm_campaign | owner_role_us_q2 | The initiative. Stable for the campaign’s life |
| utm_content | founder_post_03 | The 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:
- 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.
- 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.
- The inquiry. What was true on the visit when this particular inquiry was submitted.
Layer 1: browser observation window (first-party storage, where consent permits)
| Field | Meaning |
|---|---|
| window_id | A new ID each time a window starts |
| window_started_at | When this window started |
| window_reset_reason | Why it started: first_visit, expired, storage_cleared, new_device, consent_changed, or unknown |
| win_source / win_medium / win_campaign / win_content | Source values of the first visit in this window, including direct (stored as “(direct)”) |
| win_provenance | How that source was known: utm, referrer, click_id, none |
| click_id_type / click_id / click_at | A paid click ID (gclid, fbclid) seen in this window, and its landing-observation time |
| consent_state | granted, 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)
| Field | Meaning |
|---|---|
| contact_first_observed_source / _medium / _campaign / _content / _at / _provenance | Copied from the window linked at the contact’s first inquiry. Written once, never overwritten |
| observation_log | One row per window ever linked to this contact: window_id, started_at, win_ fields, reset reason |
| identity_merge_log | One 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 / _reason | A 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)
| Field | Meaning |
|---|---|
| inq_visit_source / _medium / _campaign / _content | Source of the visit on which the form was submitted. A direct return visit is “(direct)” |
| inq_landing_page / inq_submitted_at | Page and time of submission |
| inq_window_id | The observation window active at submission |
| carried_click_id_type / carried_click_id / carried_click_at | A paid click ID carried from earlier in the same window, with the original click time |
| self_reported_source | Candidate’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 = deniedand 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)
| Field | After visit 1 (3 Mar 10:02, Meta ad, fbclid present) | After visit 2 (6 Mar, direct, submits) |
|---|---|---|
| window_id / reset reason | W1 / first_visit | W1 (unchanged) |
| win_source / medium / campaign / provenance | facebook / paid_social / owner_role_us_q2 / utm | unchanged |
| click_id_type / click_id / click_at | fbclid / FB-EXAMPLE-1 / 3 Mar 10:02 | unchanged |
| 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
| Field | Expected |
|---|---|
| New visit 20 Jul, organic search | window W2, reset reason expired if supported by a retained expiry marker (otherwise unknown), win_source google / organic, provenance referrer |
| Second inquiry, 20 Jul | new inquiry record: inq_visit_source google / organic; no carried click |
| Identity | email matches the existing contact; identity_merge_log row: W2 → contact C-EXAMPLE-1, method “same email on form”, 20 Jul |
| observation_log | now W1 and W2 |
| contact_first_observed_* | unchanged (facebook / paid_social, 3 Mar) |
J3. Repeat inquiry from a new device, consent denied
| Field | Expected |
|---|---|
| Window | none; consent_state denied |
| inq_visit_source | from the submission URL if tagged; otherwise unknown |
| Identity | email match → merge log row; observation_log gets no new window |
| contact_first_observed_* | unchanged |
J4. Booking on an external calendar after J1’s inquiry
| Field | Expected |
|---|---|
| Booking record | carries the inquiry’s submission ID so the CRM can join it |
| Source fields | none created by the booking tool; the inquiry’s fields stay the source of record |
| Check | the join is tested separately from any cross-domain analytics linking |
Step 5: other edge cases
| Situation | Expected result |
|---|---|
| First visit direct, later tagged visit in the same window | win_source = (direct), provenance none; the tagged values are recorded on the visit, and on the inquiry if that visit submits |
| Broker introduces an existing contact | new 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”:
- Tagged link, submit on the same page.
- Tagged link, browse two pages, then submit.
- 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.
- Consent denied, tagged link, then submit.
- Direct first visit, then a tagged visit, then submit.
- Repeat inquiry from an existing contact after window expiry or on a new device (journeys J2 and J3). contact_first_observed_* must not change.
- 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