Lead Generation & Automation
How every lead is tracked end-to-end so you always know which ad, which brand, which dollar.
Date: 2026-05-04 Timeline: 2–3 weeks to full operation Scope: CallRail + GA4 + Meta Pixel + Google Ads tag + n8n automation + Google Sheets + Looker Studio
Traffic Sources (Meta, Google, Organic, GBP, Yard Signs)
|
v
Websites: azpalmtreelights.com + trimlightphoenix.com
|
+----+----+
| |
Calls Forms
| |
CallRail Fillout
| |
+----+----+
|
n8n (your n8n instance)
|
+----+----+
| |
Jobber Google Sheets (Attribution Tracker)
|
Looker Studio Dashboard
Monthly cost estimate: CallRail Essentials ~$45–75/mo + n8n (existing) + Looker Studio (free) = ~$45–75/mo incremental.
Before starting any section, confirm access to:
Meta Pixel: The pixel install for both sites is covered separately in marketing/meta-pixel-install-guide.md. Complete that guide first, then return here. Do not duplicate pixel setup in this guide.
Purpose: Track which marketing channel is driving phone calls. Each channel gets its own tracking number. Dynamic Number Insertion (DNI) swaps the number on the website in real time based on traffic source.
Timeline: 1–2 hours once account is open.
Provision one number per acquisition channel. Go to Numbers > Add Numbers > Local Number.
| Tracking Number Slot | Channel | UTM Source | Notes |
|---|---|---|---|
| AZPTL-Meta | Paid social (Meta/IG ads) | meta | Swap on site when visitor comes from fb.com/instagram.com |
| AZPTL-Google | Paid search (Google Ads) | Swap on site when visitor has gclid param | |
| AZPTL-GBP | Google Business Profile | gbp | Link from GBP listing directly to this number |
| AZPTL-Organic | Organic / Direct | organic | Default swap when no UTM present |
| AZPTL-YardSign | Yard signs / offline | yard-sign | Printed on physical signs; used in specific neighborhoods |
Steps for each number:
Note: All numbers forward to the same Quo/OpenPhone line. CallRail sits in front — it records attribution data, then rings through. Jack answers on OpenPhone as normal.
DNI is a JavaScript snippet that automatically swaps the displayed phone number on the website based on the visitor's traffic source. This is the core attribution mechanism for calls.
How it works:
<script src="//cdn.callrail.com/companies/[COMPANY_ID]/[HASH]/cr.js"
async="true"></script>
In CallRail, set up number swap rules for each tracking number:
AZPTL-Meta — Swap Rules:
facebook.com OR instagram.com OR l.facebook.commetafbAZPTL-Google — Swap Rules:
gclid (Google Ads click ID — auto-present on all Google Ads traffic)google AND UTM Medium = paid-searchAZPTL-GBP — Swap Rules:
google.com/maps OR referrer = maps.google.comAZPTL-YardSign — Swap Rules:
yard-signAZPTL-Organic — Swap Rules:
callrail.cr.js script should load.?utm_source=meta to the URL — the phone number displayed on the page should change to the AZPTL-Meta number.Set up instant call notifications so no lead goes cold:
Purpose: Track all web behavior — page views, form submissions, phone click events — and link to Google Ads for conversion tracking.
Timeline: 2–3 hours.
AZ Palm Tree Lightshttps://azpalmtreelights.com.AZ Palm Tree Lights - WebG-XXXXXXXXXX) — you will need it for GTM.Using GTM keeps all tracking tags in one place and avoids code changes for every new tag.
AZ Palm Tree Lights > Container Name: azpalmtreelights.com > Target: Web.<head>, one for <body>.WordPress:
GTM-XXXXXXX).<head> and <body>.Squarespace:
<head> GTM snippet.<body> GTM snippet.G-XXXXXXXXXX.GA4 - Configuration.GA4 fires page_view automatically via the Configuration tag. No additional setup needed. Verify in GA4 Realtime after publishing GTM.
This fires when a visitor submits the quote request form.
In GTM — create a trigger:
/quote OR Form classes contain [your form's class].Trigger - Form Submit - Quote.For Fillout embedded forms: Fillout fires a postMessage when a form is submitted. In GTM:
fillout_form_submitted (or check Fillout's GTM docs for exact event name).generate_lead.form_id — dynamic variable from Fillout's data layer pushlead_source — pull from UTM (see below)Alternative — simpler approach: If Fillout redirects to a thank-you page on submit, use a Page View trigger on that URL:
/thank-you or equals /quote-submitted.generate_lead.Fires when a visitor clicks a tel: link (tap-to-call).
In GTM:
tel:.Trigger - Phone Click.Create the tag:
phone_call_click.phone_number — set to Click URL variable.Trigger - Phone Click.GA4 - phone_call_click.This lets you see source/medium/campaign in GA4 reports per user session.
utm_source → name: UTM Sourceutm_medium, utm_campaign, utm_contentsource = {{UTM Source}}medium = {{UTM Medium}}campaign = {{UTM Campaign}}generate_lead — toggle Mark as conversion: ON.phone_call_click — toggle Mark as conversion: ON.Events only appear in the Events list after they fire at least once. Use GTM Preview mode to fire them in test, or wait for real traffic.
This is required for importing GA4 conversions into Google Ads for bidding.
gclid params automatically.After linking (allow 24–48 hours), in Google Ads:
generate_lead and phone_call_click.Purpose: Consistent UTM parameters are the foundation of the entire attribution stack. Inconsistent UTMs break every downstream report.
All UTMs are lowercase, hyphen-separated. No spaces, no caps, no special characters.
| Value | Use |
|---|---|
meta |
All Meta/Facebook/Instagram ads |
google |
Google Ads (paid search) |
gbp |
Google Business Profile organic clicks |
email |
Email campaigns (Mailchimp or manual) |
organic |
Unpaid social / SEO (when manually tagging) |
yard-sign |
Physical yard signs |
partner |
Referral partners |
| Value | Use |
|---|---|
paid-social |
Meta/IG ads |
paid-search |
Google Ads (search or PMax) |
cpc |
Alternative for Google Ads if auto-tagged |
email |
Email campaigns |
organic |
Organic search/social |
direct |
Direct traffic (rarely applied manually) |
offline |
Yard signs, truck wraps, events |
Format: [brand]_[type]_[monthyear]
| Value | Use |
|---|---|
azptl_leadgen_may26 |
AZ Palm Tree Lights lead gen, May 2026 |
azptl_retarget_may26 |
Retargeting campaign |
trim_leadgen_may26 |
Trimlight lead gen (if running) |
cx-survey-may-2026 |
Customer experience survey campaign |
azptl_brand_may26 |
Brand awareness / reach objective |
Update the month suffix when campaigns roll to a new month. Do not reuse old campaign names for new periods.
Format matches the naming used in Meta Ads Manager creative naming system.
| Value | Description |
|---|---|
c1r_day_night |
Concept 1 Remix — day/night reveal |
c11_video_loop |
Concept 11 — video loop |
c2_static_blue |
Concept 2 — static blue palm |
[concept]_[format] |
Continue this pattern |
Google Ads auto-populates {keyword} via ValueTrack. Set in ad destination URL template:
{lpurl}?utm_source=google&utm_medium=paid-search&utm_campaign=azptl_leadgen_may26&utm_term={keyword}&utm_content={creative}
Create a shared Google Sheet named "Outdoor Vibes — UTM Builder" in the shared Drive.
Sheet 1: Builder
| Column | Header | Notes |
|---|---|---|
| A | Base URL | e.g., https://azpalmtreelights.com/get-a-quote |
| B | utm_source | Dropdown: meta / google / gbp / email / organic / yard-sign / partner |
| C | utm_medium | Dropdown: paid-social / paid-search / email / organic / offline |
| D | utm_campaign | Free text |
| E | utm_content | Free text |
| F | utm_term | Free text (optional) |
| G | Final URL | Formula: =A2&"?utm_source="&B2&"&utm_medium="&C2&"&utm_campaign="&D2&IF(E2<>"","&utm_content="&E2,"")&IF(F2<>"","&utm_term="&F2,"") |
Sheet 2: URL Library
Pre-built and approved UTM URLs for all active campaigns. Columns: Date Created, Campaign, URL, Notes, Used In.
Add data validation (dropdown lists) in Sheet 1 columns B and C to enforce the approved values. Lock Sheet 2 so only Andrew can add rows.
Share: your team
utm_content with the creative name.utm_source=google&utm_medium=paid-search&utm_campaign={campaignid}&utm_content={creative}&utm_term={keyword}. This auto-populates — do not set per-ad manually.?utm_source=gbp&utm_medium=organic.Instance: your n8n instance
Setup prerequisite — Jobber: Jobber does not have native outbound webhooks. Use n8n's polling (scheduled trigger checking Jobber API for changes) or use Jobber's Zapier integration as a bridge into n8n via webhook. The recommended approach:
Jobber API credentials: Get OAuth token from Jobber Developer portal. Store as n8n credential: Jobber OAuth.
Google Sheets credential: Connect n8n to Google Sheets via OAuth. Create a dedicated service account or use Andrew's Google account.
Trigger: Fillout webhook (fires on form submit) Actions: Create Jobber lead with source fields, log row to Attribution Google Sheet
https://your-n8n-instance/webhook/azptl-form-submit (create this endpoint in n8n).n8n Workflow:
[Webhook: POST /azptl-form-submit]
|
[Set node: Map fields]
- lead_date = {{$now.toISO()}}
- name = {{$json.body.name}}
- email = {{$json.body.email}}
- phone = {{$json.body.phone}}
- source = {{$json.body.utm_source || 'direct'}}
- medium = {{$json.body.utm_medium || 'direct'}}
- campaign = {{$json.body.utm_campaign || ''}}
- content = {{$json.body.utm_content || ''}}
- notes = {{$json.body.message || ''}}
|
[HTTP Request: Jobber GraphQL API]
- Method: POST
- URL: https://api.getjobber.com/api/graphql
- Header: Authorization: Bearer {{credentials.jobber_token}}
- Body: GraphQL mutation to create a Client + Request (lead)
- Custom fields: source, medium, campaign, content
- Save returned jobber_id
|
[Google Sheets: Append Row]
- Sheet: Attribution Tracker
- Tab: Leads
- Columns: lead_date, source, medium, campaign, content, name, phone, email,
jobber_id, lead_status (set to 'new'), quote_amount (''),
won_amount (''), won_date (''), notes
Jobber GraphQL — Create Request mutation (simplified):
mutation CreateRequest($input: RequestCreateInput!) {
requestCreate(input: $input) {
request {
id
client {
id
name
}
}
userErrors {
message
path
}
}
}
Input includes client name/email/phone and request title (e.g., "Website Lead — Meta Ads"). Map custom fields for source attribution if Jobber custom fields are configured (see Section 7).
Trigger: CallRail webhook (fires when a call ends) Actions: Create/update Jobber lead, log to Attribution Google Sheet
https://your-n8n-instance/webhook/azptl-callrail.n8n Workflow:
[Webhook: POST /azptl-callrail]
|
[IF node: Call duration > 30 seconds]
- Yes → continue (real calls only; skip hang-ups and spam)
- No → stop
|
[Set node: Map source from tracking number]
- IF tracking_number == AZPTL-Meta → source = 'meta', medium = 'paid-social'
- IF tracking_number == AZPTL-Google → source = 'google', medium = 'paid-search'
- IF tracking_number == AZPTL-GBP → source = 'gbp', medium = 'organic'
- IF tracking_number == AZPTL-YardSign → source = 'yard-sign', medium = 'offline'
- ELSE → source = 'organic', medium = 'direct'
|
[HTTP Request: Search Jobber for existing client by phone]
- GraphQL query: clients(filter: {phone: $phone})
- If found: update existing client / create new request linked to them
- If not found: create new client + request
|
[HTTP Request: Jobber — Create Request]
- Title: "Phone Lead — [source]"
- Custom fields: source, medium, callrail_recording_url
- Save returned jobber_id
|
[Google Sheets: Append Row]
- Same schema as WF1
- lead_status = 'new'
- notes = "Call: " + call_duration + "s | Recording: " + recording_url
Trigger: Scheduled poll every 15 minutes — check Jobber for jobs that changed to "won" / "active" status since last poll Actions: Look up original lead source from Attribution Sheet, write revenue row
[Schedule Trigger: every 15 minutes]
|
[HTTP Request: Jobber GraphQL]
- Query jobs where status changed to ACTIVE (won) since last_checked timestamp
- Pull: job id, client id, quote total, job title, created_at
|
[IF node: any new won jobs?]
- No → stop
- Yes → continue
|
[Google Sheets: Lookup]
- Search Attribution Tracker for matching jobber_id
- If found: get original source, medium, campaign, content
|
[Google Sheets: Update Row]
- Set lead_status = 'won'
- Set won_amount = job total from Jobber
- Set won_date = today
|
[Google Sheets: Append Row — Revenue tab]
- won_date, source, medium, campaign, jobber_id, won_amount
- (This tab drives the revenue chart in Looker Studio)
State management: Store last_checked timestamp in n8n static data or a dedicated "Config" tab in the Google Sheet. Update it on every successful poll run.
Trigger: Schedule — every Monday at 8:00 AM MST Actions: Query Google Sheet, calculate KPIs, send email to Andrew
[Schedule Trigger: Monday 8:00 AM MST]
|
[Google Sheets: Read All Rows — Leads tab]
- Filter: lead_date in last 7 days
|
[Code Node: Calculate KPIs]
JavaScript:
- Total leads by source
- Won count and revenue by source
- CPL by source (if ad spend data available — see note below)
- Close rate by source (won / total leads)
- Average deal size by source
|
[Gmail / Send Email node]
To: jack@outdoorvibes.com
Subject: "Outdoor Vibes Attribution Digest — Week of [date]"
Body (HTML table):
| Source | Leads | Won | Close Rate | Revenue | CPL* |
| meta | 12 | 3 | 25% | $4,200 | $28 |
| google | 8 | 2 | 25% | $2,800 | $31 |
| gbp | 4 | 2 | 50% | $2,600 | — |
| organic | 3 | 1 | 33% | $1,400 | — |
| yard-sign | 1 | 0 | 0% | $0 | — |
| TOTAL | 28 | 8 | 29% | $11,000 | — |
*CPL = ad spend / leads. Requires manual spend input (see below).
Ad spend input: Until a Google Ads / Meta API pull is set up, maintain a "Weekly Spend" tab in the Google Sheet. Andrew manually enters spend each Monday before the digest runs. The workflow reads from that tab.
Trigger: Schedule — 1st of every month at 9:00 AM MST Actions: Compare current 30-day period to locked May 2026 baseline, send comparison email
[Schedule Trigger: 1st of month 9:00 AM MST]
|
[Google Sheets: Read — Baseline tab]
- Pull locked May 2026 baseline values (see Section 7)
|
[Google Sheets: Read — Leads tab]
- Filter: lead_date in last 30 days
|
[Code Node: Calculate deltas]
- Current leads vs baseline leads: +X% / -X%
- Current close rate vs baseline: +X pts / -X pts
- Current revenue vs baseline: +$X / -$X
- Current avg deal size vs baseline
|
[Gmail: Send Email]
Subject: "Outdoor Vibes — Monthly Attribution vs Baseline — [month]"
Body: side-by-side table — Baseline | Current | Delta
Critical: The baseline values are read-only. The workflow reads them but never writes to the Baseline tab. Lock the sheet tab (right-click tab > Protect range > your team).
File name: Outdoor Vibes — Attribution Tracker
Location: Shared Google Drive folder: Outdoor Vibes / Data
Share: your team
This is the primary data table. n8n writes every lead here.
| Column | Header | Type | Notes |
|---|---|---|---|
| A | lead_date | Date | ISO format, YYYY-MM-DD |
| B | source | Text | From UTM or CallRail lookup |
| C | medium | Text | From UTM or CallRail lookup |
| D | campaign | Text | From UTM |
| E | content | Text | From UTM |
| F | name | Text | From form or CallRail caller ID |
| G | phone | Text | E.164 format: +14806317754 |
| H | Text | From form (blank for calls) | |
| I | jobber_id | Text | Jobber request/client ID |
| J | lead_type | Text | form / phone |
| K | lead_status | Text | new / quoted / won / lost |
| L | quote_amount | Currency | Updated when Jobber quote created |
| M | won_amount | Currency | Updated by WF3 when job won |
| N | won_date | Date | Updated by WF3 |
| O | notes | Text | Call duration, recording URL, etc. |
Validation rules:
Append-only revenue log. Written by WF3 when job is won.
| Column | Header |
|---|---|
| A | won_date |
| B | source |
| C | medium |
| D | campaign |
| E | jobber_id |
| F | won_amount |
Locked. Written once during Section 7 (Baseline Capture). Contains one row per metric.
| Column | Header |
|---|---|
| A | metric |
| B | value |
| C | period |
| D | notes |
Rows: total_leads_90d, avg_monthly_leads, total_revenue_90d, avg_deal_size, close_rate, avg_monthly_calls, avg_monthly_forms
Manual entry by Andrew each Monday.
| Column | Header |
|---|---|
| A | week_of |
| B | meta_spend |
| C | google_spend |
| D | total_spend |
| E | notes |
Copy of the approved UTM URLs from Section 3.2. Read-only reference.
URL: looker.google.com (sign in with Google)
Report name: Outdoor Vibes — Attribution Dashboard
Data source: Google Sheets — Attribution Tracker (both Leads and Revenue tabs)
Share read-only with Jack: once built, use "Share > Manage Access" to add Jack's email as Viewer.
lead_date and won_date as Date type.Purpose: Weekly/monthly health check for Andrew and Jack. Answers: "Is the attribution stack working and is marketing ROI positive?"
Layout (top to bottom):
Row 1 — Scorecards (6 total)
Row 2 — Bar chart: Leads by Source
Row 3 — Two charts side by side:
COUNT_IF(lead_status = 'won') / COUNT(lead_date)Row 4 — Date filter control
Purpose: Compare channel efficiency — which source produces the best ROI?
Table: Channel Performance Summary
Dimensions: source, medium Metrics:
Time Series Chart: Leads Over Time by Source
Scatter Chart: Close Rate vs Lead Volume by Source
Filter controls on this page:
Purpose: See what's in the funnel right now — are there stale leads that need follow-up?
Funnel Chart: Lead Status Steps: new → quoted → won / lost Values: count of leads in each status
Table: Open Leads (not won or lost)
Stale Lead Count Scorecard
Won Jobs — Revenue Timeline
Purpose: Lock a pre-attribution baseline so future results can be compared against a known starting point. This is the "before" snapshot. Do it once, never change it.
Timing: Complete this before any n8n workflows go live and before any tracked campaigns run. Do it now, in the first week.
Document to create: Outdoor Vibes — Attribution Baseline — May 2026 (Google Doc in shared Drive)
Log in to Jobber with Jack present (or have Jack export). Pull the following for the period February 1 – April 30, 2026 (last 90 days as of May 1):
Also capture:
Create the Google Doc with this structure:
Outdoor Vibes — Attribution Baseline — May 2026
Locked: [date]. Do not modify.
Period: February 1 – April 30, 2026 (90 days)
Data source: Jobber export + manual count
== Lead Volume ==
Total leads (90d): [X]
Avg leads/month: [X]
Monthly breakdown: Feb [X] / Mar [X] / Apr [X]
== Conversion ==
Total quotes sent (90d): [X]
Total jobs won (90d): [X]
Quote-to-win rate: [X]%
Lead-to-win rate (if available): [X]%
== Revenue ==
Total revenue booked (90d): $[X]
Avg monthly revenue: $[X]
Avg deal size: $[X]
== Channel Attribution (Pre-Stack) ==
Primary inbound channels (estimated, not tracked): word of mouth, referral, GBP
Google Ads: not running (as of baseline date)
Meta Ads: limited / just starting
Tracking: none — all attribution manual/estimated
== Phone ==
Avg monthly calls: [X] (from Quo log if available)
Business number: (480) 631-7754
== Notes ==
[Any context — seasonal adjustments, notable events, one-time big jobs to exclude]
Outdoor Vibes / Baseline — LOCKED.Run this end-to-end test for each channel before declaring the attribution stack live. For each test, start from scratch (incognito window) to simulate a real visitor.
cr.js)Simulate: Visitor clicks Meta ad → lands on site → submits form.
https://azpalmtreelights.com/get-a-quote?utm_source=meta&utm_medium=paid-social&utm_campaign=azptl_leadgen_may26&utm_content=c1r_day_nightgenerate_lead event.phone_call_click event.https://azpalmtreelights.com?gclid=test123&utm_source=google&utm_medium=paid-search&utm_campaign=azptl_leadgen_may26Simulate: Real inbound call to a tracking number.
https://azpalmtreelights.com?utm_source=gbp&utm_medium=organichttps://azpalmtreelights.com with no UTM params.After running Tests 1–5:
Execute in this order to avoid data gaps:
Week 1 (Days 1–3):
Week 1 (Days 4–5): 6. Create GA4 property 7. Create/configure GTM container 8. Install GTM on site, add GA4 Configuration tag 9. Configure form + phone_call_click events in GTM 10. Publish GTM, verify in Realtime 11. Link GA4 to Google Ads
Week 2 (Days 1–3): 12. Create Attribution Google Sheet with all tabs 13. Build n8n WF1 (form → Jobber + Sheet) 14. Build n8n WF2 (CallRail → Jobber + Sheet) 15. Run Tests 1–5 (channel tests) 16. Fix any mapping issues
Week 2 (Days 4–5): 17. Build n8n WF3 (Jobber won → revenue row) 18. Build n8n WF4 (weekly digest) 19. Build n8n WF5 (monthly comparison) 20. Run Test 7 (digest email test)
Week 3: 21. Build Looker Studio dashboard (Pages 1–3) 22. Run Test 6 (Looker Studio end-to-end) 23. Share with Jack 24. Brief Jack: "Here's what you can see, here's what it means" 25. Apply UTM params to all active Meta ads 26. Update GBP listing URL with UTM params 27. Attribution stack is live — monitor daily for first 2 weeks
| System | URL / Access |
|---|---|
| CallRail | app.callrail.com |
| GA4 | analytics.google.com |
| Google Tag Manager | tagmanager.google.com |
| Google Ads | ads.google.com |
| n8n | your n8n instance |
| Jobber | getjobber.com |
| Jobber GraphQL API | api.getjobber.com/api/graphql |
| Jobber Developer Portal | developer.getjobber.com |
| Looker Studio | lookerstudio.google.com |
| Meta Pixel Install Guide | marketing/meta-pixel-install-guide.md |
| Attribution Google Sheet | [add URL once created] |
| Looker Studio Report | [add URL once built] |
| Main Phone (Quo/OpenPhone) | (480) 631-7754 |
| AZ Palm Tree Lights site | azpalmtreelights.com |
| Trimlight Phoenix site | trimlightphoenix.com |
Part of your Outdoor Vibes asset library — yours to keep and run.