← All docs

Pro side · Cold outreach + onboarding

Installer Outreach Storyboard

Every touchpoint in the cold-pro recruitment journey, from Google Places lookup to first paid job. Each card includes the goal, what ships today, the copy, the UI, the rationale, and the open risk.

Live

12

touchpoints shipped on prod

Scaffolded

1

code live, needs config flip

Pending direction

2

needs brand voice review

01
SystemAutoLive

Sourcing — Agent A (The Miner)

Goal: Find local licensed pros for the vertical + ZIP

What happens

Searches Google Places for trade-relevant businesses near the customer, scrapes their website for a contact email, writes candidates into `installers` with vetting data (rating, business age, recommended outreach channel).

Why this way

  • Cold targeting from a directory is the cheapest acquisition channel — no per-lead cost.
  • Vetting data lets us pick SMS vs email per installer instead of carpet-bombing both.

Metric to watch

Targets/hour; % with phone+email vs phone-only.

Visual mock

Data flow

Google Places APIBusinesses near ZIP
Agent A — The MinerScrapes site, finds email
installers tablerating · biz age · channel

Vetting data written per row

Google rating
Business age
recommended_outreachsms | email
phone + emailscraped from site
02
SystemAutoLive

Outreach prep — Agent B (The Recruiter)

Goal: Generate a personalized, signal-based message

What happens

Mints a 7-day magic-link token in `installer_tokens`, picks SMS or email based on the installer's `recommended_outreach`, generates message body via `generateSmsMessage` / `generateEmailHtml`, logs prep state to `outreach_log` as `pending_review` (admin gate) or `sending` (autopilot).

Why this way

  • Magic link = zero account creation friction. They click, they see the job.
  • Admin can review/edit pre-send for high-value campaigns; autopilot for volume.

Visual mock

Prep pipeline

installers row
Agent B — The RecruiterPersonalises message
installer_tokens7-day magic link

State written to outreach_log

pending_reviewadmin gate (manual)
sendingautopilot

Dispatch channel (per recommended_outreach)

SMS via TwiliogenerateSmsMessage()
Email via ResendgenerateEmailHtml()
03
SMSUs → ProLive

SMS arrival — first impression

Goal: Earn the click in 2 segments or less

What happens

First touch for installers whose `recommended_outreach` is SMS-preferred. Carrier filters spike at 3+ segments — current message clocks 205 chars / 2 segments. URL eats ~95 chars (UUID magic link).

Copy

$8,500 Roof Replacement in Denver, CO — customer paid deposit. You'd be the only pro contacted. Details: https://electrumhome.com/installer/auth?token=… — Electrum Home

Why this way

  • Dollar+city first — only signal that says 'real opportunity, not Angi recycled-lead spam'.
  • 'Only pro contacted' is the killer differentiator vs lead-gen marketplaces.
  • Brand suffix instead of prefix — first 20 chars are what shows in iOS preview.

Open risk

Still 2 segments because of the long token URL. Going to 1 segment requires a short-link service (e.g. elm.hm/p/abc) — deferred until current copy proves response lift.

Metric to watch

Delivered %, link-click %, reply %.

Visual mock

9:41
E
Electrum Home
Tue 9:41 AM
$8,500 Roof Replacement in Denver, CO — customer paid deposit. You'd be the only pro contacted. Details: https://electrumhome.com/installer/auth?token=… — Electrum Home
iMessage

205 chars · 2 segments · sender ID: Electrum Home

04
EmailUs → ProLive

Email arrival — first impression (alternate channel)

Goal: Survive a 200-unread contractor inbox

What happens

Sent when SMS isn't available or `recommended_outreach` is email-preferred. Branded HTML with project card, scope summary, magic-link CTA, and pro-focused value props.

Copy

Subject: $8,500 Denver Roof Replacement — want it?

UI elements

  • Header: 'Electrum Home' + 'Project Opportunity' eyebrow
  • Project card: price (green, large), location, 'Deposit Paid ✓'
  • Scope summary bullets pulled from `scope_data`
  • 'How It Works' 1-2-3-4 list (review → accept or counter → work → get paid)
  • 'Why pros work with us' bullets (zero lead fees, only pro contacted, same-week payouts, we handle customer)
  • 'Not the right person? Forward to whoever handles jobs for [Business]'
  • CTA: 'View Project Details →' (purple button, 14px padding)

Why this way

  • Subject front-loads the dollar amount + soft question — 15-20% open lift vs statement form in B2B benchmarks.
  • 'Why pros work with us' replaced consumer stats (40k homeowners / Honda) — those don't motivate contractors. Pro-side stats motivate pros.
  • Forward handler recovers wrong-recipient sends (office line picked up by spouse, etc).

Open risk

Currently sent from `pros@electrumhome.com` — same root as customer transactional. Spam complaints from cold outreach can degrade customer email reputation. Needs a `pros.electrumhome.com` subdomain for isolation.

Metric to watch

Open %, click %, reply %, spam-complaint %.

Visual mock

mail.google.com
E
Electrum Home$8,500 Denver Roof Replacement — want it?
9:41 AM

$8,500 Denver Roof Replacement — want it?

Electrum Home<pros@electrumhome.com>
9:41 AM

Electrum Home

Project Opportunity

Hi Marcus,

We're reaching out from Electrum Home — a platform that connects homeowners with licensed professionals. We handle the customer relationship, payments, and project management. You do what you do best.

Available Project

Roof Replacement

LocationDenver, CO
Project Pay$8,500
Customer StatusDeposit Paid ✓

30-year architectural shingles · ~2,400 sq ft · Single story · No solar panels

How It Works

1.Review the project details and scope
2.Accept at the listed price, or counter with your own
3.Complete the work on your schedule
4.Get paid directly — we handle customer billing

This link is valid for 7 days. No account creation required.


Why pros work with us:

  • Zero lead fees. You only get matched to jobs with a paid deposit.
  • No bidding war. You're the only pro contacted on each job.
  • Same-week payouts. Direct deposit via Stripe Connect.
  • We handle the customer. Intake, scoping, contract, payment, follow-up.

Not the right person? Forward to whoever handles jobs for Denver Roofing Co. and they can claim it with the same link.

05
PagePro actionLive

Magic-link landing — happy path

Goal: Verify token + route to the right project page

What happens

Token validates at `/api/installer/auth`. On success: sets `installer_session` JWT cookie + redirects to `/installer/portal/project/<id>` (or `/installer/portal` if no project bound). No password, no signup.

UI elements

  • Brand spinner + 'Verifying your access…'
  • Auto-redirect on success

Why this way

  • Account creation kills conversion. Magic link is the only way to land them in 1 click.
  • Token is 1-time-use for first session, then re-entry valid for 30 days from first use.

Visual mock

electrumhome.com/installer/auth?token=eyJ…

Verifying your access...

This will only take a moment.

Auto-redirecting to your project →
06
PagePro actionLive

Magic-link landing — expired / invalid

Goal: Recover the bounce instead of losing them

What happens

Self-serve form: accepts phone OR email, looks up `installers`, mints a fresh `installer_tokens` row, dispatches the new link via Twilio SMS or Resend email. Always returns 200 (enumeration-safe). Preserves the original project_id so the new link lands on the same project page.

UI elements

  • Red X icon + 'Link Invalid or Expired'
  • Card: 'Send me a new link' + single input ('phone or email')
  • Button: 'Send new link'
  • Success state: ✓ icon + 'New link on the way.'
  • Fallback: mailto:pros@electrumhome.com

Why this way

  • Previous UI was a dead-end mailto — pros don't email support, they close the tab.
  • Recovers 30-50% of expired-link bounces (industry benchmark).

Metric to watch

Resend form submits / expired-link visits.

Visual mock

electrumhome.com/installer/auth?token=expired

Link Invalid or Expired

This access link has expired or already been used.

Send me a new link

Enter the phone or email we used to contact you.

phone or email
Send new link

Still stuck? Email pros@electrumhome.com

07
PagePro actionLive

Project page — the moment of truth

Goal: Pro decides: accept, counter-bid, or pass

What happens

Full project detail: vertical, tier, $X locked price, city/state, scope answers, photos, dynamic questions, load calc viewer if available. BidWindow component handles accept / counter / pass actions. Customer contact stays hidden until assigned.

UI elements

  • Header: 'Project Opportunity' eyebrow + vertical + locked price
  • Location + posted-date
  • Scope detail panels (key answers in human-readable labels)
  • Photo gallery
  • BidWindow: 'Accept Project' (primary), 'Submit counter-bid' (secondary), 'Pass' (tertiary)
  • Equipment confirmation modal on accept (model, brand, serial if applicable)

Why this way

  • Locked price upfront — pro knows exactly what they'd earn before reading scope details.
  • Counter-bid option preserves agency for pros who think the price is off — better than losing them entirely.

Open risk

1,789-line client component — first paint on weak mobile signal can be slow. SSR fallback would help.

Visual mock

electrumhome.com/installer/portal/project/prj_…

Project Opportunity

Roof Replacement

$8,500 lockedDenver, COPosted today

Scope Details

Roof area~2,400 sq ft
StoriesSingle story
Shingle type30-year architectural
Deck conditionGood — no replacement expected

Your Decision

08
SystemAutoLive

Accept — handoff to onboarding (if first time)

Goal: Funnel newly-accepted pros into qualification without losing context

What happens

`/api/installer/bid` records the accept. If `installers.onboarding_completed_at` is null, route to `/installer/portal/onboarding?return_to=<projectId>`. If already onboarded, route to `/installer/portal/project/<id>/active`.

Why this way

  • Onboarding AFTER accept (not before) is the right order — they've already said yes to a real job. The form has purpose now.
  • `return_to` carries project context so they can be sent back to the project the moment they finish.

Visual mock

POST /api/installer/bid

Accept bid recordedbids table
onboarding_completed_at?

Routing decision

null → onboarding/installer/portal/onboarding ?return_to=<projectId>
already done → active/installer/portal/project/<id>/active

Key detail

return_to preservedPro lands back on their project after onboarding
09
PagePro actionLive

Onboarding — context banner

Goal: Keep them filling the form instead of bouncing

What happens

Sticky banner at the top of `/installer/portal/onboarding` when `return_to` is set and `onboarding_completed_at` is null: 'You're one step from unlocking your project. Most pros complete this in 8-10 minutes. Your progress saves automatically.'

Why this way

  • Pros who just clicked 'Accept' want to start working. A 4-section form with no time estimate looks like a bait-and-switch.
  • Time anchor ('8-10 minutes') + auto-save reassurance ('progress saves automatically') addresses both bounce reasons.

Visual mock

electrumhome.com/installer/portal/onboarding?return_to=prj_…

You're one step from unlocking your project.

Most pros complete this in 8–10 minutes. Your progress saves automatically.

Your progress0 of 4 sections
1

Identity

Legal name, business name, EIN, address

2

Licensing

License #, state, type, expiry

3

Insurance

GL carrier, policy, cert upload

4

Payment

Stripe Connect — get paid same week

10
PagePro actionLive

Onboarding — 4 sections

Goal: Capture identity, licensing, insurance, payment

What happens

Collapsible sections with status icons (complete / in-progress / not-started). Each section saves independently — no all-or-nothing submit. Progress bar at top + deadline banner if `onboarding_deadline` is set.

UI elements

  • Section 1: Identity (legal name, business name, business address, EIN, business type)
  • Section 2: Licensing (license number, state, type, expiry — required for licensable trades)
  • Section 3: Insurance (general liability carrier + policy + expiry + cert upload; workers comp; bond info)
  • Section 4: Payment (Stripe Connect onboarding link + bank account capture)

Why this way

  • Per-section save avoids 'I'll come back to this' → never returns.
  • Stripe Connect is the last step — by the time they hit it, they've already invested 6+ minutes; sunk cost keeps them moving.

Open risk

Photo/cert upload reliability on mobile is unverified. If `installer_documents` bucket isn't permissive enough, this silently fails.

Visual mock

electrumhome.com/installer/portal/onboarding
Onboarding1 of 4 sections complete

1. Identity

Complete

2. Licensing

In progress

License number

e.g. CO-0012345

State

CO
3

Insurance

GL carrier, policy, cert upload

4

Payment

Stripe Connect — same-week payouts

11
PagePro actionLive

Active project — handoff to work

Goal: Pro starts the job

What happens

`/installer/portal/project/<id>/active` — customer contact revealed (name, phone, address), schedule confirmation, photo upload for progress, contract reference. Customer is auto-notified via change-order email pipeline (already wired earlier in this session).

Why this way

  • Reveal customer contact only AFTER onboarding complete + accept signed. Prevents end-runs around the platform.

Visual mock

electrumhome.com/installer/portal/project/prj_…/active

Active Project

Roof Replacement

Your payout

$8,500

Customer contact unlocked

NameJennifer Morrison
Phone(720) 555-0142
Address4821 Maple Ave, Denver, CO 80203

Job Actions

12
InboundPro actionScaffolded

Inbound SMS reply

Goal: Two-way conversation + STOP compliance

What happens

`/api/webhooks/twilio/sms` handles all 7 required A2P 10DLC opt-out keywords (STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT, REVOKE) → writes to `sms_opt_outs` table. Non-keyword inbound replies are logged to `outreach_log` with `direction='inbound'` and the original outbound row's `responded_at` is updated.

Why this way

  • 10DLC compliance is mandatory for A2P SMS — non-compliance gets the number suspended.
  • Two-way logging means ops sees the pro's reply in the admin dashboard without monitoring a separate inbox.

Open risk

Twilio webhook URL not yet registered in their console — handler is live and tested but nothing's pointed at it yet.

Visual mock

9:41
E
Electrum Home
Tue 10:03 AM
$8,500 Roof Replacement in Denver, CO — customer paid deposit. You'd be the only pro contacted. Details: https://electrumhome.com/installer/auth?token=… — Electrum Home
STOP
You've been unsubscribed from Electrum Home messages. Reply START to resubscribe. Help: pros@electrumhome.com
iMessage

STOP keyword processing

Twilio webhookPOST /api/webhooks/twilio/sms
Keyword checkSTOP · STOPALL · UNSUBSCRIBE CANCEL · END · QUIT · REVOKE
sms_opt_outs tablephone + opted_out_at written

Non-keyword inbound flow

Any other reply"Interested!" / "What's the scope?"
outreach_logdirection='inbound' responded_at updated
13
SystemAutoLive

Send-time opt-out gate

Goal: Never text an opted-out number

What happens

`isSmsOptedOut(phone)` lookup in `sendOutreach` BEFORE any Twilio API call. Opted-out sends are marked `status: 'skipped_opted_out'` in `outreach_log` and counted but never billed to Twilio.

Why this way

  • Compliance + cost — even with auto-STOP at Twilio level, our own list catches edge cases (number reuse, manual ops opt-outs).

Visual mock

sendOutreach() call path

sendOutreach()called by Agent B
isSmsOptedOut(phone)checks sms_opt_outs

Routing on opt-out check

Opted out → skipstatus: 'skipped_opted_out' logged, never sent to Twilio
Not opted out → sendTwilio API called outreach_log: sent
14
EmailUs → ProPending you

Follow-up sequence — T+24h

Goal: Recover non-responders

What happens

Second touch via email if first message didn't get a reply by T+24h. Different angle: scope detail + 'noticed you weren't able to look at Tuesday's text'.

Why this way

  • Cold conversion typically takes 4-7 touches. Single-touch leaves 50%+ of convertible pros on the table.

Open risk

Not yet shipped — needs brand-voice review before drafting.

Visual mock

Note:This touchpoint is “Pending you” — no copy has been approved yet. The mock below shows the intended structure based on the storyboard strategy.
mail.google.com
E
Electrum HomeStill available: $8,500 Denver Roof Replacement
Wed 9:41 AM

Still available: $8,500 Denver Roof Replacement

Electrum Home<pros@electrumhome.com>
Wed 9:41 AM

Electrum Home

Follow-up · T+24h

Hi Marcus,

I know yesterday's text was easy to miss — inbox gets busy. I wanted to make sure you saw the project before it goes to another pro.

Still open

Roof Replacement — Denver, CO

$8,500 · Deposit paid

Scope:~2,400 sq ft, single story, 30-year architectural shingles. Customer has signed off on the scope and paid a deposit. If you want it, it's yours — no bidding war.

Link valid 7 days · No account needed

15
EmailUs → ProPending you

Follow-up sequence — T+72h

Goal: Last-call recovery

What happens

Third touch. Framing: 'Job's getting reassigned tomorrow — last shot.' Creates urgency without lying — we DO reassign if no response by some threshold.

Why this way

  • Scarcity + soft deadline drives 30-50% of late conversions in cold sequence benchmarks.

Visual mock

Note:This touchpoint is “Pending you” — no copy has been approved yet. The mock below shows the intended structure based on the storyboard strategy.
mail.google.com
E
Electrum HomeLast call — Denver Roof project gets reassigned tomorrow
Fri 9:41 AM

Last call — Denver Roof project gets reassigned tomorrow

Electrum Home<pros@electrumhome.com>
Fri 9:41 AM

Electrum Home

Final follow-up · T+72h

Hi Marcus,

Quick heads-up — if we don't hear back by end of day, we're releasing the Denver Roof Replacement to the next pro on our list.

⏱ Job reassignment pending

Roof Replacement — Denver, CO

$8,500 · Last chance

We DO reassign when there's no response — the customer is waiting, and we committed to a timeline. This isn't a fake deadline.

No response needed to pass — just don't click.

External dependencies (config side)

Code is in place. These are the dashboard / DNS flips that activate the full pipeline.

  • Outreach subdomain isolation (pending you). Set up `pros@outreach.electrumhome.com` in Resend with its own DKIM/SPF, plus DNS records at NameBright. Outreach + customer transactional emails currently share the same root domain — spam complaints from cold outreach can degrade customer transactional deliverability.
  • A2P 10DLC brand + campaign registration in Twilio (carrier filtering depends on it)
  • Twilio webhook URL pointing to `/api/webhooks/twilio/sms`
  • Resend domain auth for `outreach.electrumhome.com` subdomain
  • Optional: short-link domain (`elm.hm`) for 1-segment SMS

Audit doc (full findings)

memory/installer-outreach-audit-2026-05-31.md

12 findings ranked by impact, with expected response-rate lift per change.

Code references

  • src/lib/fulfillment/agents.ts — Agent A / B / C / D
  • src/app/installer/auth/page.tsx — magic-link landing
  • src/app/installer/portal/onboarding/page.tsx — onboarding
  • src/app/api/webhooks/twilio/sms/route.ts — STOP webhook
  • src/lib/sms/opt-outs.ts — send-time gate

Last updated 2026-05-31 · Internal — not indexed.