👋 Welcome to BookBack AI
BookBack AI is the in-house CRM for managing leads, jobs, finance, contractors, and service areas across all offices. This help center walks through every feature, with live UI previews you can compare to what's on your screen.
The system is split into a small number of pages, plus shared panels (SMS & Tasks) that slide in from the left on any page. You don't need to memorize anything — most flows are click-driven.
The main pages
- Leads — every prospect from every source. Where the day-to-day phone work happens.
- Jobs — booked appointments. Created from Leads or directly. Has its own statuses and webhooks.
- Finance — the money side of Deposit/Done jobs: payouts, mark-paid, finance-cancel. Owner + finance_admin edit; viewer read-only.
- Contractors — directory of who covers what areas + job types. Owner + finance_admin can edit; admin, agent, and viewer are read-only.
- Service Areas — geographic pins (50-mile radius each) that contractors opt into. Owner + finance_admin can edit; others read-only.
Shared panels (every page)
- 📱 SMS — every CTM-routed SMS conversation, with reply, templates, and STOP/UNSUBSCRIBE handling.
- 📋 Tasks — to-do items assigned to you or your office, optionally linked to a lead or job.
Where to start
If you're new to the system: read the Leads dashboard chapter first — that's where you'll spend most of your time. After that, the chapters are independent; jump around as needed using the sidebar.
🔑 Login & roles
Every action in the system is gated by your role. Knowing yours tells you what you can and can't do.
Roles
| Role | Typical user | What they do |
|---|---|---|
| Owner | You / leadership | Everything, including the Control Panel, Users, Automations, hard-deletes, and deleting jobs. |
| Admin | Office manager / dispatcher | Full Lead + Job management: status changes, K-confirm, rebook, tags/notes, and archive/restore leads & jobs. Cannot open Finance or the Control Panel, and cannot edit Contractors/Service Areas. |
| Finance Admin | Bookkeeper / payments | Everything Admin can do on Leads & Jobs, plus the Finance dashboard (edit closing/parts/deposit/profit %, mark contractor/partner paid, finance-cancel, archive from Finance) and can manage Contractors & Service Areas. Cannot open the Control Panel. |
| Agent | CSR on the phones | Works leads end-to-end (claim, status, notes, SMS, create) and runs jobs day-to-day: create a job, K-confirm, edit operational fields (name, phone, email, address, schedule, service area, contractor, notes), and change status — In Progress / Deposit / Pending / Done / Cancelled (entering the totals Done requires, picking a reason Cancelled requires). Archiving, deleting, and editing money fields on the Finance dashboard stay admin+. Contractors/Service Areas are read-only; cannot archive leads. |
| Viewer | Auditor / read-only stakeholder | Read-only on every page they reach, including Contractors and Service Areas. (Note: viewers can edit Finance fields — see the full matrix in CP → Permissions.) |
| Partner | External lead/job partner | Sandboxed, read-only. Sees only the Leads and Jobs tied to their partner name; client details are visible but contractor phone/email are hidden. No access to Finance, Contractors, Service Areas, or the Control Panel. |
Page access by role
| Page | Owner | Admin | Finance Admin | Agent | Viewer | Partner |
|---|---|---|---|---|---|---|
| Leads | ✓ | ✓ | ✓ | ✓ | read | read* |
| Jobs | ✓ | ✓ | ✓ | read +create | read | read* |
| Finance | ✓ | ✗ | ✓ | ✗ | read | ✗ |
| Contractors | ✓ | read | ✓ | read | read | ✗ |
| Service Areas | ✓ | read | ✓ | read | read | ✗ |
| Help | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Control Panel | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
✓ = full access · read = view-only · read +create = Agents view jobs and can create them, but editing them is admin+ · read* = Partner sees only records tied to their partner name (contractor contact hidden).
Per-office Jobs feature toggle
Owner CP → ⚙️ Office Settings has a 🔧 Jobs Feature switch per office. When off for an office:
- Users assigned only to that office can't open
/jobs,/contractors, or/service-areas— they redirect back to/leads. - Top-nav links to those pages are hidden for them.
- Reports modal hides the Jobs tab for them.
- The office's existing jobs are excluded from
/api/jobsqueries, so they vanish from the dashboard until you flip the toggle back on (rows stay in the DB intact). - Converting a lead in that office keeps the legacy "🔧 Mark as Opened in Workiz" reminder.
When the toggle is ON, converting a lead in that office instead opens a new job in the CRM: the lead is marked Converted and the agent is taken to /jobs with the New Job form pre-filled from the lead (see Converting a lead). So a converted lead never touches Workiz for jobs-enabled offices.
Use this when an office isn't ready for the Jobs flow — currently set up so we can run Jobs on Garage/Sliding/General while Joes Doors FL stays on the manual Workiz flow.
Office assignment
Each non-owner user is assigned one or more offices (e.g. Sliding, Garage, General). Office filters everything you see:
- You only see leads tagged with one of your offices.
- Jobs you create default to your office.
- Tasks scoped to "My Office" filter to your assigned offices.
Owner sees every office. Multi-office users get a comma-separated assignment (managed in CP → Users).
🧭 The topbar
The horizontal strip at the top of every page. Same layout on Leads, Jobs, Contractors, and Service Areas.
What each item does
- Page nav (Leads / Jobs / Contractors / Service Areas / Help) — the active one is highlighted blue. Items hidden if your role can't access that page.
- Role badge — color-coded (orange owner, blue admin, green agent, gray viewer) so you can tell at a glance what permissions you have.
- 📱 SMS — opens the SMS slide-in panel. Pulses red when there are unresolved inbound messages. AI-handled threads stay quiet unless the AI escalates.
- 📬 Meta — opens the Meta (Messenger / Instagram) inbox slide-in. Pulses purple on unread DMs. Mutually exclusive with SMS (opening one closes the other).
- 🤖 AI Assistant — opens the in-app AI chat panel. Lets staff query the CRM in plain English ("how many fresh leads do we have in Joe's Doors FL?"). Streams responses, surfaces every tool call it made for transparency, audited and budgeted in CP.
- 📋 Tasks — opens the Tasks slide-in panel. Pulses red with a count badge when you have tasks due today or overdue.
- 📊 Reports — owner-configured email reports + on-demand summary.
- 👥 Users — directory of users in the system; admins+ can edit, viewers see read-only.
- 📋 Timeline — global activity feed (every status change, claim, etc.) — admins/viewers/owners only.
- 📋 Audit Log — owner-only. Every system event for compliance/forensics.
- ⚙️ Control Panel — owner-only. The big settings hub — everything from sources to automations to recurring tasks.
- 🔕 / 🔔 Notifications — toggles browser desktop notifications for new leads.
- Sign Out — clears your session and returns to the login screen.
🏢 Offices explained
"Office" is the org-bucket every lead, job, and most settings get tagged with. It's the most important concept after roles.
An office is just a name like Sliding, Garage, General, or Joes Doors FL. It doesn't represent a physical location — it represents a routing bucket. Owners manage the list in CP → Offices.
What office tags do
- Visibility — non-owner users only see leads/jobs whose office matches one of their assigned offices.
- Phone & email routing — each office has its own CTM number (for SMS automations) and SMTP credentials (for email automations). Set in CP → Office Settings.
- Webhook payloads — outbound webhooks (Workiz / Deposit / Cancelled / New Lead) include the office field so downstream tools can branch by it.
- Automation conditions — every automation rule can scope to one or more offices.
- Online Booking embeds — each embed maps submissions to a specific office.
How an office gets onto a record
- Inbound lead webhooks — the source's webhook config sets the office (or defaults to
General). - Manual lead create — admins/owners can set it; agents inherit their first assigned office.
- Online Booking — every embed has a fixed office.
- New job — the New Job modal has an Office dropdown at the top; defaults to your assigned office.
📋 Leads dashboard
The default landing page. A filterable list of every lead from every source.
| Time | Name | Phone | Source | Status | Urgency | Office |
|---|---|---|---|---|---|---|
| 10 min ago | Big Lou | +1305••••••• | Facebook Ads | Fresh | High | Sliding |
| 2 h ago | Carmen Diaz | +1786555•••• | Yelp | Needs a Follow Up | Medium | Garage |
| Yesterday | Mike Patel | +1954555•••• | Google Ads | Converted | Low | Garage |
| 2 days ago | Sarah Kim | +1305555•••• | Online Booking Tool | Not Yet Scheduled | Medium | Sliding |
Columns
Office · Time Received · Full Name · Phone · TZ · Metro · Source · Status · BookBack AI · Next Contact · Actions.
- Full Name — capped at 140px with ellipsis truncation. Hover to see the full name in a tooltip. Frees horizontal space for the Metro column.
- Metro — closest major US metro derived from the lead's ZIP prefix (e.g. "Atlanta, GA" / "Los Angeles, CA"). ~150 ranges covering all major USPS Sectional Center Facilities. Empty for unmapped prefixes.
How rows are sorted
- Overdue follow-ups first (Needs a Follow Up / Consultation Call + next-contact in the past)
- Fresh leads with no scheduled callback
- Not Yet Scheduled / Needs a Follow Up / Consultation Call with a future callback
- Converted
- Irrelevant
- Inside each tier: most recent
tr(time received) first
The filter row
- Office / Status / Source / Urgency — multi-select dropdowns. Pick any combination.
- 🔀 Transferred — toggle to show only transferred leads (yellow accent).
- ⚡ BookBack — toggle to show only BookBack-AI-reactivated leads (cyan accent).
- Date range — preset (Today / Last 48h / Last 7 / Last 30 / This month / etc.) or custom range.
- Search — fuzzy match across name, phone, email, campaign, ZIP.
Opening a lead
Click any row to open the right-side detail panel. The panel slides in over half the table; the lead's full info is editable inline (where your role allows it). Press Esc or click the ✕ to close.
Pagination
The table loads 25 rows at a time by default. The bar below the table has Prev/Next and shows "21–25 of 478 leads." Filters update the total. There's no infinite scroll.
Real-time updates
The page polls for new leads every few seconds. When a fresh lead arrives:
- It appears at the top of the list
- The page title flashes a count:
(2) Leads — GDQ - If you've enabled browser notifications (🔕 button → 🔔), you get a desktop ping
🚦 Lead statuses & urgency
Every lead has a status and a derived urgency. The urgency drives both the row sort order and (in a moment) the visual color.
The five statuses
| Status | Means | Urgency |
|---|---|---|
| Fresh | Brand new — never been worked. Default for incoming leads. | High |
| Not Yet Scheduled | Made contact, didn't book yet. Use the next-contact timer to schedule a callback. | High when due, Medium otherwise |
| Needs a Follow Up | Active follow-up needed (left voicemail, awaiting reply, etc.). | High when due, Medium otherwise |
| Consultation Call | A consultation call is booked with the lead. A date+time picker sets when. Sorts and surfaces exactly like Needs a Follow Up. | High when due, Medium otherwise |
| Converted | Booked into a job. Closing. | Low |
| Irrelevant | Disqualified (wrong service, prank, can't service the area, etc.). | Low — fires the Cancelled Leads webhook on transition |
Urgency rules
- High — Fresh, OR (NYS / Needs FU / Consultation Call with next_contact ≤ now)
- Medium — NYS / Needs FU / Consultation Call with a future next_contact
- Low — Converted or Irrelevant
Urgency is computed live, not stored. The same row with a 9am callback shows as Medium at 8:55am and High at 9:05am — the page recomputes on every render.
Changing status
- Open the lead detail panel (click the row).
- The Status field is a dropdown — pick the new value.
- For Not Yet Scheduled, Needs a Follow Up, or Consultation Call: a date+time picker appears so you can set the next callback.
- For Irrelevant: a reason field appears (mandatory). Goes into the timeline + the Cancelled Leads webhook payload.
- Status change saves on blur — no Save button.
📞 Claiming a Fresh lead
Fresh leads have a hidden phone number until someone "claims" them. This prevents two CSRs from calling the same lead.
The claim button
On a Fresh lead, the phone field is blurred and replaced with a "📞 Show Number & Claim Lead" button. Clicking:
- Reveals the phone number for everyone (the blur clears)
- Stamps the lead with your name + timestamp ("Claimed by Omri")
- Adds a timeline entry:
Lead claimed by Omri - Other agents see a "📞 Omri" badge next to the phone
The claim is informational — it doesn't lock the lead. If two CSRs both click within seconds, the second click wins and the timeline shows both events. The expectation is that you call the lead immediately after claiming.
What unclaims it
A claim auto-clears when the lead leaves Fresh (status changes to NYS, Needs FU, Converted, etc.). The phone unblurs for everyone at that point.
Other agents' claims
You can see who claimed each Fresh lead from the dashboard — but the phone stays blurred to you until you claim it yourself. This is intentional friction so the claim is meaningful.
⏱ Snooze sequences
Per-source rules that auto-set the next-contact time after each call attempt. So you don't have to think about "when should I retry?".
How it works
Each source (Facebook, LSA, Yelp, etc.) has a configured sequence of intervals. There are three built-in defaults:
- Standard (Facebook / Google / LSA / Website / Nextdoor / Manual) — 22 attempts over 17 days. Cadence: 4 calls in first 12h + 2 in next 12h on day 1+2, repeat for day 3+4, then daily at varying clock hours for 7 days, then 3 calls every 48h.
- Aggressive (Angi / Thumbtack / Yelp) — 14 attempts: 5min · 10min · 20min · 30min · 1h · 2h · 3h · 12h · 1d · 1d · 1d · 2d · 2d · 20d.
- BookBack AI — 14 attempts: 15min × 7 then 8h × 7.
When you log a call attempt on a lead (the dial pad button on the detail panel), the system reads the lead's source, looks up the right interval based on call count, and sets next_contact = now + interval. The lead drops off your "due now" list and reappears at the right time.
adjustToCallingHours() shifts any computed snooze that would land outside the configured calling hours into the next valid window — so a 3 AM slot moves to ~9 AM automatically.
After the cap (22 attempts on Standard, 14 on the others) the next "Tried to Call" press surfaces an Irrelevant prompt instead of scheduling another snooze.
Where to configure
- Owner CP → General Settings → Snooze Sequences
- Each source has its own row. Click Edit to set intervals.
- Intervals are minutes; a small editor lets you add/remove steps with units (min/h/d).
next_contact. You'd need to set the next contact manually.📦 Archive & restore
Admin and above (admin, finance_admin, owner). Archiving is a soft-delete — the lead and its full timeline stay in the database; it's just hidden from the main dashboard. Agents and viewers don't see the archive button.
- Open the lead, click the 📦 Archive icon in the detail-panel header (next to ✕).
- Confirm. The lead disappears from the main list immediately.
- To restore: Leads top-bar → 📦 Archived → the Archived Leads modal lists every archived lead → click ↩ Restore.
Archived leads keep their full timeline + history. The Archived Leads modal also has an owner-only 🗑 Delete that permanently removes a lead — that one cannot be undone.
✅ Converting a lead → a job
What happens when you set a lead to Converted depends on whether its office has the Jobs feature on.
Jobs-enabled offices (Garage / Sliding / General)
- Set the lead to Converted — it's marked Converted right away.
- You're taken straight to the Jobs page with the New Job form open, pre-filled from the lead: name, phone, email, office, source, and notes (the lead's ZIP + requested service are dropped into the notes).
- Confirm the address (pick it from the Google suggestions so it resolves a service area), then set the schedule, Job Partner, job type, and contractor, and click Create Job.
- The job links back to the lead, and a "🛠 Job JOB-#### created from this lead" note lands on the lead's timeline.
If you don't finish creating the job, the converted lead shows a green "🛠 Open Job Creation" panel in its detail view so you can re-open the pre-filled form anytime. No Workiz step is involved for these offices.
Jobs-disabled offices (Joes Doors FL)
Converting keeps the legacy manual flow: the lead is marked Converted and shows the 🔧 Workiz reminder — open the job in Workiz, then click "Mark as Opened in Workiz" to record the Workiz job number on the lead.
⚡ BookBack AI reactivation
When a lead that's already in the system is hit again by BookBack AI, the existing record is "reactivated" — not duplicated.
What triggers it
BookBack posts to /webhook/leads with source=bookback. The system matches the incoming phone (last 10 digits, ignoring formatting) against existing leads. If it finds a match:
- The existing lead is updated, not duplicated
- Status resets to Fresh (calls counter zeroed)
- Urgency forced to High
- BookBack's notes are appended to the existing notes (with a timestamp header)
- A timeline entry:
⚡ Reactivated by BookBack AI — was Converted (Yelp) - A bookback_calls row is recorded (with playbook score, objection, recording URL when available)
Filtering BookBack leads
The dashboard filter row has a ⚡ BookBack toggle (cyan). Click to show only leads where bookback=true.
🔍 Duplicate cleanup
Owner tool for cleaning up phone-or-FB-ID collisions across leads. Reachable from the topbar 🔍 Check Duplicates button.
What it finds
- Leads with the same last-10 phone digits
- Leads with the same Facebook lead ID (rare since cross-source dedup was removed)
What it lets you do
- Open the duplicates modal — groups of matched leads appear, sorted newest first.
- For each group: choose which lead to keep (radio button). The others get archived; their timelines copy onto the keeper as "Merged from L-XXXX."
- Click Resolve per group, or Resolve all to apply your selections in batch.
🛠 Jobs dashboard
Booked appointments. Open to every logged-in role for viewing; write actions still gated to admin+. Hidden entirely for users whose offices all have the Jobs feature toggled off (CP → Office Settings).
| Job ID | Office | Date & Window | Client | Address | Type | Status | Reb? | K? |
|---|---|---|---|---|---|---|---|---|
| 📅 Today · Thursday, Apr 30 · 2 jobs | ||||||||
| JOB-3601 | Sliding | Apr 30 · 2pm–4pm | Mike Patel | Repair | IP🔵 ASK FOR UPDATE | — | K | |
| JOB-3602 | Garage | Apr 30 · 4pm–6pm | Carmen Diaz | Install | Submitted | — | ✓ K | |
| 📅 Tomorrow · Friday, May 1 · 3 jobs | ||||||||
| JOB-3603 | Sliding | May 1 · 9am–11am | Sarah Kim | Repair | DEP | 🔁 | K | |
Columns
- Job ID — formatted as
JOB-3542and up. Counter starts at 3542 (business choice). - Office — the org bucket. Frozen at create time.
- Date & Window — the appointment time range. The list groups by day with a thicker divider; today's divider is highlighted blue.
- Client — first + last name.
- Phone — the customer's number.
- Address — the job site address (truncated).
- Job Type — pill (Repair, Install, etc.).
- Company — the brand name being represented to the customer.
- Contractor — the assigned contractor, OR a blinking 🔍 FIND CONTRACTOR button if unassigned (hidden for Done/Cancelled).
- Status — short-code pill (S / IP / DEP / P / D / C). Hover for the full name. May show an attention badge below.
- Rebooked? — 🔁 if the job has been rebooked at least once.
- K? — green K button (or ✓ K if confirmed). See K-confirmation.
- Created — relative time ("3 days ago").
Filter row
- Search — fuzzy match across customer name, phone digits, address, job number (display_id), and contractor name.
- Multi-filter dropdowns — Status / Job Type / Job Source / Office / Contractor. Each accepts multiple selections. The Contractor dropdown has a built-in search and a
— Unassigned —entry. - Sort — within each date group, choose Status (Submitted → IP → Pending → Deposit → Done → Cancelled), Time, or Contractor A–Z. Date groups themselves stay chronological.
- Date filter — All Dates / Today only / Today · -3D / +7D (default) / This week / This month / Next month. Past dates outside the window are hidden so the page doesn't pile up.
- 🚨 Needs Attention (red) — toggles to attention-only view. Bypasses the date filter so flagged jobs show regardless of when. Forces every date row expanded.
- 🔁 Rebooked (blue) — toggles to rebooked-only view (jobs with
rebooked_atset). - ✕ Clear filters — appears whenever any filter is active. Resets search, multi-filters, attn / rebooked toggles, and the date filter back to "All Dates."
Sorting & date dividers
Rows sort chronologically by appointment time (date_start ASC, time_start ASC), then by status priority within a day. Above each new date a thicker divider says e.g. 📅 Today · Thursday, Apr 30 · 3 jobs. Empty days don't appear.
Collapsible dates
Each divider has a ▼/▶ toggle on the left. Past dates auto-collapse by default; today and future dates start expanded. Click the arrow to flip an individual date. When a collapsed date contains an attention-worthy job, a blinking 🚨 NEEDS ATTENTION pill surfaces on the divider so it stays visible.
Expand / collapse all: the ▶/▼ chevron next to the Job ID column header flips every visible date group at once — first click expands them all, once everything's open it collapses them all. (Hidden while the Needs Attention filter is on, since that forces every date open.)
📋 EOD Reminders + 🌅 Morning Reminders
Each date divider also has two blue buttons on the right:
- 📋 EOD Reminders — opens a modal with one row per contractor scheduled that day. Each row has a contractor-specific Google Static Map (numbered red pins) and the long-form text — Job#, Customer, Phone, Address, Job Type, Notes, Date+Time — labelled "Reminder for Tomorrow." Per-row Copy Image / Copy Text buttons. Use the night before for next-day dispatch.
- 🌅 Morning Reminders — same modal structure, short-form text ("1. ***6AM-8AM*** address"), labelled "Reminder for Today." Use the morning of for the day's reminder.
Both flows pre-resolve any job missing lat/lng via the Places find-from-text endpoint, so Static Maps never needs the Geocoding API. Maps Static API must be enabled in Google Cloud Console for the configured key.
The "Needs Attention" filter
The 🚨 button at the top pulses red when there's at least one row needing action. The button shows the live count: 🚨 Needs Attention (4). Click to filter to only those rows (which also forces every date open). A row needs attention when:
- Deposit + expected-completion date in the past → 🔴 UPDATE NEEDED (clickable — copies a "What's the update please?" message with the deposit-collected date for that contractor)
- Pending + follow-up time passed → 🟣 FOLLOW UP NEEDED
- In Progress + (job end + N hours) passed → 🔵 ASK FOR UPDATE (clickable — copies the same "What's the update please?" template with the scheduled time window). N defaults to 4h and is tunable in CP → Jobs Attentions.
- Active jobs without a contractor → 🟡 FIND CONTRACTOR (Submitted / IP / Deposit / Pending only — Done/Cancelled with no contractor are historical and don't blink)
All four use the same blinking pulse so they breathe in sync. Clicking ASK FOR UPDATE or UPDATE NEEDED copies a ready-to-paste contractor message:
🔔Update Needed🔔 🏷️Job #21434 - Emily Harrison 📍 1379 Algiers St, Pt Charlotte, FL 33980 🕒 5/3 9AM-11AM - DUE What's the update please?
➕ Creating a job
Click + New Job in the topbar. A wide modal opens with a 2-column form.
Available to agents and up — the CSR who books the appointment can create the job (and the convert-a-lead flow drops them straight into this form, pre-filled). Once the job exists, editing it — status changes, K-confirm, archive — is admin+.
Step-by-step
- Pick the office. Office sits at the top of the modal, defaulted to your assigned office. It scopes everything else.
- Fill in client info. First name + phone are required. Phone goes through US validation — placeholders like
1231231234are rejected. - Type the address. Google Places autocomplete suggests as you type. Pick a suggestion to auto-fill city/state/ZIP behind the scenes. The mini-map in the right column zooms to the address.
- Pick the service area. The 5 closest service-area pins appear as radio cards on the right. Click one — its area code + office address auto-fill.
- Pick the job type and source. Both dropdowns. Required.
- Pick the contractor (optional). The dropdown is filtered to contractors who cover the chosen service area AND handle the chosen job type. If you click the dropdown without picking area + type first, a tooltip points at what's missing.
- Set the date & time window. Date pickers + 15-minute-step time pickers. The end time auto-fills as start + 2 hours.
- Click Create Job. The job lands as
Submitted, gets aJOB-####ID, fires the Workiz webhook, fires the BookBack-jobs webhook, fires any matchingjob_createdautomations.
Outside-click protection
Clicking outside the modal (on the dark backdrop) doesn't close it instantly — you get a "Close without saving?" confirm so a stray click doesn't wipe a half-filled form.
🚦 Job statuses (deep dive)
Each status has different required fields when transitioning to it, and each fires (or doesn't fire) different webhooks.
| Status | Required fields when entering | Webhook fires |
|---|---|---|
| Submitted | None — the default after create. | workiz_webhook_url + bookback_jobs_webhook_url (on creation) |
| In Progress | Note (optional) | None |
| Deposit | Note (mandatory) + Expected completion date + Deposit amount | deposit_webhook_url |
| Pending | Note (mandatory) + Follow-up date+time. The detail panel's Follow-up section has a ⏰ Reschedule Follow-up button (agent+) to push the next check-in to a new date/time — logged to the timeline. | None |
| Done | Closing total + Parts total | done_jobs_webhook_url |
| Cancelled | Cancellation reason (mandatory) | cancelled_jobs_webhook_url |
Re-opening a Cancelled job
Cancelled jobs aren't dead. The status buttons stay visible in the detail panel; clicking another status (e.g. back to Submitted) re-opens the job and fires the appropriate webhook for the new status. A warning notice appears: "⚠ This job is cancelled. Picking another status will re-open it and fire the webhook."
The detail panel attention banner
If a job is in attention state when you open it, the top of the detail panel shows a red/purple/cyan banner explaining what's wrong. Disappears once you take action. The 🔴 / 🔵 banners are clickable and copy a "What's the update please?" message to the clipboard for sending to the contractor.
Schedule changes go to the timeline
Editing a job's date_start / time_start / date_end / time_end from the detail panel writes a 📅 Schedule changed: 2026-05-02 09:00–11:00 → 2026-05-03 14:00–16:00 entry to the job timeline so anyone reviewing later sees what moved and when. The 🔁 Rebook flow logs its own 🔁 Rebooked entry so the two paths show up distinctly.
✅ K-confirmation
"K" stands for Konfirmed (or Kept-the-job, depending who you ask). It's a one-click signal that the assigned contractor accepted the appointment.
Why it exists
Sending a job to a contractor is one thing — getting them to confirm they're actually taking it is another. The K column gives the dispatcher a single visual marker per row: green-K-button for "still waiting" vs ✓-K for "confirmed."
The flow
- Click the green K button in the row.
- A small dialog opens: "Did Mike Hernandez accept the job at 123 Main St on Apr 30, 2:00pm?" with a contractor dropdown.
- If the assigned contractor accepted: leave the dropdown alone, click Mark as Accepted.
- If a different contractor took it: pick them from the dropdown. Confirming both flips the K AND reassigns the job (and re-snapshots Profit Share).
- If no contractor is assigned yet: the dialog asks "Which contractor accepted?" — pick one and confirm.
The K button flips to ✓ K and a timeline entry is written: ✅ K-confirmed — Mike Hernandez accepted the job.
Un-K
Click ✓ K → "Clear K-confirmation?" → confirm. The flag clears, the row needs K again. Useful if a contractor confirms then backs out.
K resets on rebook
When a job is rebooked, the K-confirmation always clears — even if the rebook didn't change the contractor. The reasoning: the schedule changed, so the contractor's prior acceptance is no longer authoritative.
🔁 Rebooking
Mark a job as rebooked when the appointment moves OR the contractor changes. Click the 🔁 Rebook button in the detail panel header.
The dialog
Two checkboxes:
- Change date / time — when checked, reveals new start date + time + end date + time pickers (15-min steps). Pre-filled with the current values.
- Change contractor — when checked, reveals a contractor dropdown filtered by area + job type. Pre-selected to the current contractor.
You can check either, both, or neither. "Neither" still flags the job as rebooked (timeline marker only) — useful if you just want a paper trail that something shifted out-of-band.
What happens on save
- The rebook timestamp + user are stamped (
rebooked_at/rebooked_by). - Date/time changes apply.
- Contractor change applies AND profit share re-snapshots from the new contractor.
- K-confirmation clears.
- Timeline marker:
🔁 Rebooked — new window May 5 9am · new contractor: Joe Smith · K-confirmation cleared. - The 🔁 icon appears in the Rebooked? column on the dashboard.
🔍 Find Contractor
For unassigned jobs, the Contractor column shows a yellow blinking 🔍 FIND CONTRACTOR pill instead of "—".
Click it (or open the job and use the Edit flow on the contractor field) to get a small dialog with a dropdown of contractors who cover the job's service area + handle the job type. Pick one → assigned. The pill goes away, the contractor name appears in the column, profit share snapshots, and a timeline entry is written.
This isn't K-confirmation — it's just assignment. The K button stays in its "needs K" state until you separately mark it.
For Done and Cancelled jobs, the pill is suppressed (replaced with quiet "Unassigned" italic text) since the work is historical.
📦 Archiving a job
Admin and above (admin, finance_admin, owner). A soft-delete that hides the job from the dashboard, the top-bar counters, and the duplicate-address check while keeping the row + timeline in the database. Use it for test, duplicate, or dead jobs you don't want cluttering the board.
Archive a job
- Open the job; click the 📦 Archive icon in the detail-panel header (next to ✕).
- Confirm. The job drops off the dashboard immediately and a note is written to its timeline.
View & restore
Top-bar → 📦 Archived opens the Archived Jobs modal (display ID, status, address, partner, archived date). Click ↩ Restore to put a job back on the dashboard. The Archived button shows for owner + admin.
Archiving from Finance
The same job archive lives on the Finance dashboard for owner + finance_admin: 📦 in the job side-panel header to archive, top-bar 📦 Archived to view/restore. An archive made anywhere is reflected everywhere — one timeline note and one audit-log entry. Finance viewers are read-only and don't see the buttons.
🔄 Auto Status Change
Owner CP → Jobs Settings → 🔄 Auto Status Change. Rules that move jobs from one status to another automatically on a schedule, enabled per office.
The first rule: Submitted → In Progress
When enabled for an office, a Submitted job in that office flips to In Progress at 7 PM local time the day before it's scheduled. "Local" means the job's own timezone, so the change rolls across the country: 7 PM EST handles EST jobs, 7 PM CST handles CST jobs, and so on — by 10 PM EST (= 7 PM PST) every next-day job is In Progress.
- A job scheduled for today that's somehow still Submitted is advanced on the next check (its 7 PM-yesterday threshold has passed).
- Past-dated jobs are never auto-advanced.
- The transition writes a timeline note ("⏱ Auto-advanced by the … rule") and fires the same
job_statusautomations a manual change would.
Per-office control
Each rule has an Enabled toggle plus a row of office checkboxes. Tick only the offices you want it to run in — any office you leave unchecked keeps manual status changes only. (Disable the rule entirely and it stops everywhere.)
How often it runs
A background job checks every 15 minutes, so a transition lands within ~15 minutes of the 7 PM local threshold. Owner-only to configure; the transitions run on their own.
📥 Importing jobs from Workiz
The 📥 Import button (top bar, owner + admin) bulk-creates jobs from a Workiz export (.xlsx or .csv). Columns are read by header name, so column order doesn't matter.
Column mapping
| Workiz column | Becomes |
|---|---|
| Job # | Ignored — a fresh JOB-#### is issued. |
| Client | First + Last name (a middle name/initial stays in First name). |
| Scheduled | Start date + time. End = start + 2 hours. |
| Phone | Phone. |
| Status | Mapped to a CRM status. Pending gets a follow-up due in the next hour (adjust it later). |
| Tech | Contractor — auto-matched by name; unmatched ones you map (or leave unassigned). |
| Address + City | Combined into the full address (+ State/Zip if present). |
| Source | Job Partner — auto-matched; unmatched ones you map. |
| Type | Job Type — auto-matched; unmatched ones you map. |
The flow
- Click 📥 Import → choose the Workiz file.
- Pick the Office for the batch (and optionally a Company name; defaults to the office).
- Resolve any unmatched Tech / Source / Type via the dropdowns — each maps once per distinct value.
- Click Import Jobs. You get a count of created jobs plus a per-row list of any that were skipped (e.g. a row with no partner or an unreadable date), so you can fix and re-import just those.
Imported jobs are tagged with an "Imported from Workiz" timeline note and do not fire the job webhooks/automations (they're historical, not new live events).
💰 Finance dashboard
The /finance page — the money side of jobs. Open to owner, finance_admin (both can edit), and viewer (read-only). Plain admins and agents are blocked on purpose; partners have no access.
What it shows
Only jobs in Deposit or Done status (the ones with money to track), office-scoped like everywhere else. Status changes made on the Jobs dashboard flow here live.
Editing the numbers
Click a job to open the side panel and inline-edit Closing, Parts, Deposit, and Profit %. The Contractor Payout recomputes as (closing − parts) × (1 − profit%). Viewers see the numbers but the inputs are locked.
Marking paid
- Contractor paid / Partner paid — K toggles per job, stamped with who and when.
- House-brand auto-K — Done jobs whose partner sits at 100% keep-rate get partner-paid stamped automatically (tooltip shows "Auto").
Finance-cancel
Owner / finance_admin can mark a Done/Deposit job finance-cancelled (chargeback, refund, dispute) with a reason — independent of the job's real status, which stays Done on the Jobs board. Reversible with ↺ Undo cancel. Cancelled jobs surface in the contractor reimbursement sweep on the next weekly report.
Archive
The 📦 in the side-panel header archives a job (owner + finance_admin); the top-bar 📦 Archived lists archived jobs to restore. Shared with the Jobs dashboard — see Archiving a job.
Reports
📊 Reports sends weekly per-contractor and per-partner payment summaries (with reimbursement line items for finance-cancelled jobs). Finance SMTP + the outbound CTM number are configured in CP → Finance settings (owner).
👷 Contractors page
Open to every logged-in role for viewing; only owner can edit. The directory of who covers what areas + handles what job types.
Layout
- Left sidebar — search box, industry filter pills, scrollable contractor list. Each row shows the contractor's name, primary contact, area count, industry pills, an Active toggle, and Edit/Areas buttons.
- Right map — every service area as a colored pin. Coverage density coloring: red = 0 contractors, yellow = 1, green = 2+. Hover a pin → list of contractors covering it. Click a contractor row → only their areas highlight blue, others dim.
Adding a contractor
Click + Add Contractor in the topbar. Modal fields:
- Name (required) — the company name
- Email (Reports) — where reports go
- Contact Person + Phone — primary contact info
- 2nd Contact Person + 2nd Phone — optional backup
- Notes — free-form
- Job Types (required) — checkbox list. The contractor is matched to jobs only if their job type matches.
- Service Areas (optional — can edit later) — checkbox list with a search box. You can save a contractor with no areas yet and wire them up via the 🗺 Areas map editor afterwards.
- Profit Share % + Secondary % + Zipcodes for Secondary + Job Partners for Secondary — see Profit Share
- Active checkbox — uncheck to take the contractor out of New Job dropdowns without deleting them
Inline Active toggle
Each row has a small "✓ Active" / "○ Off" pill. Click without opening the modal to toggle — useful for quickly pausing a contractor.
🗺 In-place service area editing
A faster way to add/remove a contractor's service areas without opening the full edit modal.
The flow
- Click the 🗺 Areas button on a contractor row.
- A blue banner appears above the map: "Editing coverage for Mike Hernandez · Click pins to toggle · Done."
- The contractor's covered pins flip to vivid blue. Uncovered pins show as a clear, clickable gray.
- Click any pin to add/remove that area. The change saves immediately (optimistic UI; rollback on failure).
- Click Done on the banner — or click any other contractor — to exit edit mode.
Hover popovers and the area InfoWindow are suppressed during edit mode so clicks land cleanly. Profit share doesn't auto-recompute (areas don't drive that — ZIPs do). Entering edit mode no longer re-zooms the map — it stays where you left it so you can edit in context.
Priority zones (drawn polygons)
Separately from per-pin coverage, you can draw a polygon zone on the map and give it an ordered list of contractors — the priority order dispatch should try them in. Open a zone (or draw a new one) to get the editor, which has a Job Type and an add-contractor dropdown. Admin and above can create/edit/delete zones.
- The add-contractor dropdown is searchable — type to filter; only active contractors eligible for the chosen job type appear, and the field starts empty (no pre-selected contractor).
- Keyboard add: after typing, press ↓/↑ to move through matches and Enter to add the highlighted contractor straight to the list (same as clicking + Add). The menu stays open so you can add several in a row.
- Reorder the priority list with the ▲/▼ buttons; remove with ✕.
📥 Contractor bulk upload
Upload an Excel/CSV with up to 1,000 contractor rows at once. Click 📥 Bulk Upload in the topbar.
The columns
| Col | Field | Notes |
|---|---|---|
| A | Contractor Name | Required. Used for upsert match. |
| B | Contact Person | |
| C | Phone | |
| D | 2nd Contact Person | Optional |
| E | 2nd Phone | Optional |
| F | Email (reports) | |
| G | Profit Share % | 0–100 |
| H | 2nd Profit Share % | 0–100 |
| I | Zipcodes for 2nd % | Comma-separated |
| J | Job Partners for 2nd % | Comma-separated (e.g. "GDQ Leads, Nehoray Leads"). Case-insensitive. |
| K | Notes | Optional, free-form |
Upsert by name
Names are matched case-insensitively. Existing contractors get their contact info / profit-share fields refreshed; industries and service areas are NEVER touched on update so manually-curated coverage survives a re-upload. Brand-new names insert as fresh rows with empty industries + areas.
What new rows need next
Bulk-uploaded contractors don't have job types or service areas yet — they won't appear in New Job matching. Open each one and tick the right job types + service-area pills to wire them up. The in-place areas editor is the fastest way.
📍 Service Areas page
Open to every logged-in role for viewing; only owner can edit. The 50-mile-radius pins that contractors opt into.
Layout
- Left sidebar — search box + a sortable list of areas. Each row shows label, city/state/ZIP, area code.
- Right map — every area as a blue dot. Hover or click a dot → details popover with city/state/ZIP + the count of contractors covering it + a Coverage by Job Type breakdown.
Adding an area
- Click + Add Service Area.
- Enter a label (free-form, e.g. "Hialeah") and the main ZIP code (5 digits).
- Click the Lookup button — it geocodes the ZIP through Google Places to derive city, state, lat, and lng.
- Optionally fill in Main Area Code + Office Address. These appear on the New Job modal when this area is picked.
- Save.
📥 Service Area bulk upload
Excel/CSV upload of up to 500 areas. Click 📥 Bulk Upload.
The columns
- A — Service Area (label)
- B — Main Zipcode (5 digits)
- C — Main Area Code (optional)
- D — Office Address (optional)
Each ZIP is geocoded via Google Places to derive city, state, lat & lng. Rows with errors are listed at the end of the upload — fix them in the spreadsheet and re-upload. Duplicate labels are refused.
Outside-click protection
The bulk upload modal asks "Close Bulk Upload?" if you click outside it after picking a file. Same pattern as every other modal in the system.
📱 SMS panel
Slide-in inbox for every SMS routed through CTM. Available on every page via the 📱 SMS button in the topbar.
The conversation list
Threads are grouped by (our CTM number, customer phone). Each row shows:
- Linked lead's name (if matched) — bolded blue
- Customer phone number (only when no lead match)
- Our line: the CTM number + label + office
- Last message snippet with direction arrow (← inbound / → outbound)
- Unread badge (count of unresolved inbound messages)
- Relative time of last message
Filters
- Search — fuzzy match across message body, lead name, phone digits
- 🏢 Offices — multi-select dropdown to scope to one or more offices
- Unread only — toggle to hide resolved threads
- Clear all — appears once any filter is active
Inside a thread
- Reply — textarea at the bottom. Enter sends, Shift+Enter adds a new line. 160-char counter for SMS-segment awareness.
- 📎 Attach — image / video / audio / PDF up to 4MB. Sent as an MMS through CTM.
- 📋 Templates — quick-reply popover with pre-saved text snippets (managed in CP → SMS Templates).
- ● Unread — flips the thread back to unread (useful when you read but want to come back to it).
- ✓ Resolve — clears the unread badge for the thread. New inbound messages reopen it automatically.
Reply-disabled forwarding numbers
Some CTM numbers are configured "no-reply" by an admin (CP → CTM Numbers → toggle). Threads on those numbers show the inbound history but the reply form is replaced with a notice ("This forwarding number doesn't accept replies").
Toll-free outbound SMS is blocked
Numbers with toll-free area codes (800 / 833 / 844 / 855 / 866 / 877 / 888) require a separate TCR / 10DLC carrier registration before they can send SMS. Until that's set up, the reply form on those threads is replaced with a 📵 notice: "Toll-free number — SMS not available. Use a different number to text this client back, or call them." Server-side /api/sms/reply also returns 403 for any attempt to send from a toll-free line.
Live sync
The SMS panel polls /api/last-change every second. When the server timestamp moves it triggers a fresh load of the conversation list AND, if a thread is open, the thread itself — so inbound messages and outbound sends from other agents appear within ~1 second. The 📱 button's unread badge updates in the same tick.
🚫 STOP / opt-out
TCPA compliance — when a customer texts STOP / UNSUBSCRIBE / CANCEL / END / QUIT / OPTOUT, we add their phone to a suppression list and refuse future automation SMS to them.
How detection works
The first whole word of every inbound SMS is checked. So:
- "STOP" → opt-out
- "STOP texting me" → opt-out (first word matches)
- "stopwatch?" → ignored (not a whole-word match)
- "please stop" → ignored (STOP isn't first)
What happens on opt-out
- The phone (last 10 digits) is added to
sms_suppressions. - The matched lead (if any) gets a timeline marker:
🚫 SMS opt-out — recipient replied "STOP". - Any future automation that tries to SMS this number is refused at send time and logged as failed in the automation log.
- Manual SMS replies from the panel are NOT blocked — they're considered intentional 1:1 outreach.
Re-opt-in
Customer texts START / UNSTOP / YES / SUBSCRIBE / OPTIN / OPT-IN — phone is removed from the suppression list and the lead's timeline gets ✅ SMS opt-in.
📋 Tasks panel
Slide-in to-do list. Same position + behavior as the SMS panel. Pulses red with a count when you have due/overdue tasks.
Three scopes
- My Tasks — tasks assigned to you specifically OR to one of your offices. Default tab.
- My Office — only tasks assigned to your offices (not your specific user).
- All — admin/owner only. Every task in the system.
Creating a task
Click + New. The editor modal asks for:
- Title (required) — what needs to be done
- Description — optional details
- Due Date & Time — datetime-local picker
- Assign To — radio: User (specific person) OR Office (anyone in that office; first to mark Done wins)
Linking to a lead or job
Open any lead — there's a 📋 button in the detail panel header next to Archive. Click → task editor opens pre-filled with the lead link. Same flow on jobs (📋 + Task button next to Short / Full / Rebook).
The created task has a clickable lead/job pill in the panel; the lead/job's timeline gets 📋 Task created: "..." on creation and ✅ Task completed: "..." on done.
Permissions
- Create — any non-viewer.
- Edit / delete — creator + assignee + admins.
- Mark done / re-open — anyone non-viewer (any teammate can finish a shared task).
Cascade-delete on parent gone
If a linked lead or job is deleted, the task auto-deletes too — the work no longer applies.
🔁 Recurring tasks
Owner-configured templates that auto-generate fresh tasks daily / weekly / monthly. Owner CP → General Settings → 🔁 Recurring Tasks.
Template fields
- Title + description
- Cadence — Daily / Weekly (pick day of week) / Monthly (pick day of month)
- Time of day — 15-min steps
- Assignee — User OR Office
- Active — pause without deleting
How generation works
A cron tick runs every 15 minutes. For each active template:
- Computes "now" in EST so day-of-week / day-of-month math matches the dispatcher's calendar
- Decides if today is a firing day (always for daily; matches dow for weekly; matches dom for monthly)
- Skips if the configured time-of-day hasn't passed yet
- Idempotency-stamps via (template_id, occurrence_key) — never double-fires (occurrence_key is e.g.
2026-05-01for daily,2026-W18for weekly,2026-05for monthly) - Inserts a fresh task row with due_at = today's date at the configured time
Generated tasks are normal tasks — assignees can mark Done as usual. Already-generated instances stay untouched if you delete the template later.
🤖 AI Assistant chat
In-app conversational AI for staff. Click the 🤖 AI Assistant button in the topbar to open the chat panel. Ask anything about the CRM — leads, jobs, contractors, conversion rates, recent calls — in plain English. The assistant uses tools to query live data, streams answers as it goes, and shows every tool it touched.
What it can do
- Live data lookups — leads by office / status / source / urgency, job pipeline, contractor history, recent CTM call activity, dashboard counts. It reads the same numbers you see in the UI.
- Exact counts — "how many contractors / leads / jobs do we have" returns the true total, not a capped sample. (It can list up to a few hundred rows per query when you ask it to enumerate.)
- Multi-step reasoning — "compare last week's conversion rate vs the week before" → runs both queries and explains the delta.
- Tool transparency — every query the model ran appears as a chip above the reply. Click a chip to expand the raw input/output the assistant saw. No black-box answers.
- Markdown tables — when the answer is naturally tabular ("top 5 sources this month") it renders a styled table inline.
Daily budget & audit
- Each role has a daily spend cap (default $2/day). Configurable in CP → 🤖 AI Assistant → Phase 4: Budget & Audit. When you hit your cap the next message returns a 429 toast — resets at midnight Eastern.
- Every assistant turn is logged with tokens used, cost, and the exact tools called. Owner can open CP → 🤖 AI Assistant → Audit log to replay any past conversation.
- Owner picks the model in the same CP panel — Sonnet 4.6 (balanced default), Haiku 4.5 (cheaper, faster), or Opus 4.7 (best, expensive).
Who can use it
Any authenticated user (viewer and up). The data the assistant returns respects the user's office scope — an agent in Joe's Doors FL only gets numbers for their own offices, never cross-office data.
📬 Meta inbox (Messenger / Instagram)
Slide-in panel that shows every Facebook Messenger and Instagram DM from connected Pages and IG Business accounts. Same shape as the SMS panel — opens with the 📬 Meta button on any page.
How threads land here
- Owner configures a Meta App (one app, many Pages) and connects each Page in CP → 📬 Meta Accounts. The page's long-lived access token is stored encrypted; the account is tagged to one or more offices.
- Customer sends a DM to a connected Page or IG account. Meta POSTs the webhook to
/webhook/meta. - We dedupe by Meta message id, persist the inbound, and bump the topbar 📬 badge.
Visibility & scoping
Strictly by the Meta account's office. A user in Joe's Doors FL sees only threads on Pages tagged Joe's Doors FL — they don't see Garage office DMs even if the same customer's phone is a lead in their office (this was a security hole that's been closed). Owners with office all see everything.
Handler badges in the thread list
- 🤖 AI (purple) — a Meta AI agent is actively handling this thread.
- 🤖→👤 HANDED OFF (amber) — the AI escalated; a human needs to take it from here.
- No badge — pure human-handled thread (no agent configured for that Page, or agent is paused).
Inside a thread
- "via Page · platform" header sticks to the top while you scroll. Shows handler state as a pill.
- 🚨 HANDED OFF banner appears below the header when the AI escalated, with the reason it gave ("customer asking about military discount", etc.).
- 👤 Take over / 🤖 Hand back to AI button in the header (only when the Page has an agent). Take over freezes the AI; hand back re-engages it and fires an immediate turn if the last message is an unresolved customer inbound.
- DRY-RUN DRAFT bubbles (amber dashed) — when an agent runs in dry-run, its replies persist here as drafts but no Messenger message goes out.
- 📁 Archive / 📂 Restore tabs at the top of the list for tidying old threads.
Reply window
Meta's policy: free-form replies are allowed only within 24 hours of the customer's last inbound. After 24h, the textarea is replaced with a 🔒 notice. The customer must DM us first to re-open the window.
Live sync
The panel polls every 12 seconds. Inbound messages and outbound sends from teammates appear within ~12s, and the topbar 📬 badge updates in the same tick.
📩 Meta AI Agents
Auto-reply bots for Facebook Pages and Instagram accounts. One agent per Page (CP → 🤖 Meta AI Agents). When a customer DMs a Page with an active agent, the AI generates a reply, optionally sends via Messenger.
Setup
- First connect the Page in CP → 📬 Meta Accounts (one-time).
- Then go to CP → 🤖 Meta AI Agents — every connected Page appears as a card.
- For each Page you want auto-reply on: set Status = Active, pick a persona template (Garage Doors / HVAC / Plumbing / Electrical / Locksmith / Cleaning / Generic), tune the prompt, save.
- Keep Auto-send OFF at first — agent generates DRAFTS (amber bubbles in the Meta inbox). Flip Auto-send ON only after the drafts read right.
Behavior on inbound DM
- Agent has a small read-only toolset:
find_customer_history(phone),check_service_area(zip),escalate_to_human(reason). - Hands off to a human when: customer says "real person" / "manager" / "stop", agent calls
escalate_to_human, N AI replies cap hit (default 6), or N minutes elapsed since agent took over (default 10). - Cost is tracked per agent — TODAY / THIS MONTH / ALL-TIME shown at the top of each card.
Why it's currently blocked
Meta requires App Review approval on the pages_messaging permission before real customer DMs flow through our webhook. Until the app is approved and switched Live, only Meta-dashboard Test events come through — real DMs don't reach us. Submission was made 2026-05-10; typical review window is 1–4 weeks.
💬 SMS AI Agents
Auto-reply bots for SMS. Unlike Meta (one agent per Page), here one agent can serve many CTM numbers. You create the agent persona once, then assign it to whichever CTM numbers should use it.
Setup
- CP → 💬 SMS AI Agents → "+ Create agent". Pick a persona template, prompt asks for your business name and substitutes it into the template. Save.
- CP → 📞 CTM Numbers → in the "🤖 AI Agent" column, pick your agent from the dropdown on each line you want it on.
- Set Status = Active in the agent card. Keep Auto-send OFF until you've tested.
- Make sure the CTM line has completed A2P 10DLC registration (TCR brand + campaign + number attached). Without this, US carriers reject every outbound SMS with "Message cannot be delivered until A2P campaign is registered." This is a CTM-side / carrier compliance setup, not anything in our app.
Toolset
check_service_area(zip)— does the company cover this ZIP? Returns the matched service-area rows.get_recent_lead_activity()— pulls the matched lead's recent timeline events. Useful when the customer references prior contact.check_existing_jobs()— agent calls this BEFORE booking. If an active job exists for the customer's phone, the agent routes to a human instead of booking a duplicate.book_appointment(...)— creates a real lead in your dashboard once the agent has full name, phone, address, date, time window, issue notes (email optional). Tagged withsource=smsagentand the CTM line's office. Idempotent: one booking per thread per 24 hours.book_consultation_call(...)— the price-only fallback when the customer refuses an on-site. Creates a lead with first name, phone, zip, callback date/time.escalate_to_human(reason)— agent hands off and stamps the reason on the thread so a teammate knows why.
Grace window — new vs returning customer
- New-customer grace (default 120s) — agent waits this long after an inbound before replying, giving a teammate room to take over manually first.
- Returning-customer grace (default 300s = 5 min) — if the inbound's phone matches any existing lead in the CRM, the longer wait applies so a teammate has more time to step in personally on existing relationships.
- Duplicate-resend suppression — if the customer texts the same message twice within 30s (the "I don't think it delivered, let me resend" pattern), the timer doesn't reset.
Follow-up scheduler
If the customer goes silent mid-conversation, the agent follows up automatically:
- #1 fires 15 minutes after the AI's last outbound
- #2 fires 30 minutes after #1
- #3 fires 24 hours after #2
- After #3 — no more chasing.
What cancels pending follow-ups: customer replies (timer re-arms only after the agent's next reply), human Take Over, AI escalation, customer texts STOP, or a booking succeeds.
An ⏰ FOLLOW-UP #N/3 → chip pins to the top of the thread showing the relative + absolute time. No chip means no follow-up is armed (booking succeeded, taken over, escalated, or the customer is mid-conversation).
Visual signals in the SMS inbox
- "via …" header — sticky, shows the CTM line + handler pill.
- 🤖 AI HANDLING / 🤖→👤 HANDED OFF — handler pill, sticky with the header.
- 🚨 HANDED OFF banner — appears below the header when the AI escalated, with the reason. The SMS topbar badge stays lit on escalated threads (auto-resolve is skipped on escalation specifically so you notice).
- ⏰ FOLLOW-UP #N/3 chip — counts down to the next follow-up if one is queued.
- DRY-RUN DRAFT bubble (amber dashed) — agent's draft reply when Auto-send is OFF. No SMS goes out.
- 🤖 AI bubble (purple) — agent's reply that was actually sent via CTM.
- [failed] bubble — CTM rejected the send (A2P, line disabled, etc.). The error is in the body so you can see what went wrong.
Auto-resolve behavior
When the agent replies (sent or drafted), all prior unresolved inbounds in the thread are auto-marked resolved — same as a human reply does. This keeps the SMS topbar badge from ringing forever on AI-handled threads. Exception: on escalation, we intentionally leave the inbound unresolved so the badge rings and you notice the thread needs you.
Cost counter
Each agent card shows TODAY / THIS MONTH / ALL-TIME spend plus reply count. Calculated per AI call from tokens_in × in_price + tokens_out × out_price for the model in use. NY-local day/month boundaries.
If a booking fails
The agent's book_appointment call hits a DB error or validation issue → the failure path emails every active owner with the conversation context + the collected fields, so a teammate can finish the booking manually. The agent itself tells the customer "I'm having trouble locking it in, let me grab a teammate" and escalates.
🌐 Online Booking — overview & flow
Embeddable widgets that drop on customer websites. Submissions land as Leads with source Online Booking Tool, tagged to the office configured for that embed.
The 3-step customer flow
- Service. Pick from the cards (or skipped entirely if 0 services configured). Each card can have a name, optional price, optional image.
- Date & Time. 7-day pill strip with previous/next-week arrows; past days greyed out. Pick a day → 2-hour windows from open to close-2 appear.
- Your Info. First name (required) · Last name · Phone (required, US-validated) · Email · Address (Google Places autocomplete). A hidden honeypot field protects against bots.
What happens on submit
- Lead inserted with
source=online_bookingand the embed's office - Notes carry the requested window + UTM params (if the host page passed any) + referrer
- Existing lead-creation hooks fire: new_lead_webhook_url, lead_created automations, etc.
- Page either shows the configured thank-you message OR redirects to the configured URL (mutually exclusive)
Test mode
Append ?test=1 to any embed URL to click through the full flow without inserting a real lead. The submit step short-circuits and the thank-you page renders anyway. Useful for the customer to verify the embed before going live.
🛠 Embed builder
Owner CP → Automations & Integrations → 🌐 Online Booking Tool. Each office gets one (or more) embeds.
Required fields
- Internal Name — what the embed is called in the CP (not shown to customers).
- URL Slug — the path piece, e.g.
sliding→https://gdq-leads.onrender.com/book/sliding. Must be unique. - Office — every submission is tagged this office.
- Open Hour + Close Hour — drives the time slots. Need at least 2 hours between them.
Branding
- Page Title — overrides the default "BOOK ONLINE" header (full-page mode only).
- Company Name — shown in the form header.
- Brand Color — color picker. Drives the primary button + step indicator + accent colors.
- Logo — file upload (PNG / JPG / SVG up to 200 KB). Stored as a base64 data URL.
Services
Up to 3 rows. Each: name + optional price + optional image upload. Leave all blank to skip the Service step entirely (the customer goes straight to Date & Time — fastest possible flow).
"What to expect" sidebar
Up to 3 callouts with icon + title + body. Shown next to the form on desktop only (collapses below on mobile and is hidden in inline mode).
Confirmation mode
Radio choice — mutually exclusive:
- Show a thank-you message — multi-line text, displayed on a confirmation card.
- Redirect to a thank-you page — full URL. The page navigates the parent window (works through iframes too).
Domain whitelist
Comma-separated list of host domains that may load this embed. Empty = any domain. Useful so a competitor can't iframe your widget.
Other actions per row
- ✓ Live / ○ Paused toggle
- 📋 Embed code — opens a panel with full-page URL + inline iframe snippet, both with copy-to-clipboard
- ↗ Preview — opens
?test=1in a new tab - ⎘ Duplicate — clones the row (paused, suffixed "(copy)") so you can tweak a variant fast
- Edit / Delete
📋 Embedding on a website
Click 📋 Embed code on any embed row. Two formats are offered.
Full-page link
The customer navigates to your booking URL directly. Use it as a button on their site:
<a href="https://gdq-leads.onrender.com/book/sliding" target="_blank">Book an appointment</a>
Inline iframe
Drops the form right onto the customer's page. Adjust height if the bottom gets cut off:
<iframe src="https://gdq-leads.onrender.com/book/sliding?inline=1" style="width:100%;max-width:540px;height:720px;border:none" title="Book an appointment"></iframe>
UTM passthrough
If the host page links to the embed with UTMs (?utm_source=fb&utm_campaign=spring), they ride through to the lead's notes for attribution. The embed also captures document.referrer.
🤖 How automations work
Owner CP → Automations & Integrations → 🤖 Automations. Rules that fire SMS or email when a job/lead event happens.
Anatomy of an automation
- Trigger — what event fires it (job created, status changed, time before appointment, etc.)
- Conditions — optional filters (office in [...], status in [...], job type in [...])
- Recipient — who receives the message (assigned contractor / client / static address)
- Channel — SMS or Email
- Template — body (and subject for email) with
{{variables}}
Sender identity
- SMS — uses the office's CTM number (configured in CP → Office Settings).
- Email — uses the office's SMTP credentials.
Without those configured, automations for that office fail at send time and log failed in the log.
Quick example
"When a job is created in Garage office, SMS the assigned contractor with the appointment details":
- Trigger = Job Created
- Condition: Office = Garage
- Channel = SMS · Recipient = Assigned Contractor
- Body =
Hi {{contractor.name}}, new job {{job.displayId}} on {{job.dateStart}} at {{job.timeStart}} — {{job.address}}. Client: {{client.fullName}} {{client.phone}}.
⚡ Trigger types
Three groups: job events, lead events, time-based.
Job triggers
- Job Created (Submitted) — fires once on creation
- Job Status Changed — pick the target status (Submitted / In Progress / Deposit / Pending / Done / Cancelled)
- Job Rebooked
- Contractor Assigned — when a contractor is set on a job that didn't have one OR the contractor is changed (Find Contractor / K-confirm reassign / Rebook with new contractor)
Lead triggers
- Lead Created (any source) — fires for manual creates, bulk imports, AND every inbound source webhook
- Lead Status Changed — pick target (Fresh / Not Yet Scheduled / Needs a Follow Up / Consultation Call / Converted / Irrelevant)
Lead triggers cannot use the "Assigned Contractor" recipient — leads don't have one. The editor disables that option automatically when a lead trigger is picked.
Time-based triggers
Cron tick every 5 minutes:
- X time before Job appointment — e.g. 24 hours before
date_start + time_start - X time after Job end window — e.g. 2 hours after
date_end + time_end - X time after Lead created — e.g. 1 hour after
tr
Each rule has an offset: a number + unit (minutes / hours / days). Idempotency-stamped so the same (rule, job/lead) pair never fires twice.
🎚 Conditions
Every condition is multi-value (OR within a row, AND across rows). Empty = no constraint.
- Office — filters on the row's office
- Job Type — job triggers only
- Job Source / Lead Source — label adapts to the trigger context
- Current Status — filters on the row's status at evaluation time. Distinct from
trigger_status(which is the status BEING ENTERED for *_status triggers). Lets you say e.g. "24h before start, only if still Submitted."
Example: "Garage repairs only"
Office = Garage · Job Type = Repair. Two rows AND'd together — fires only when both match.
Example: "Any service except installs"
Currently no NOT operator. Workaround: list every type EXCEPT install in the Job Type row. Future v3 work.
✏️ Templates & variables
Body (and email subject) support {{variable}} substitution. Empty/missing variables render as empty string.
Available variables
Job (job triggers only):
{{job.id}} {{job.displayId}} {{job.address}} {{job.city}} {{job.state}} {{job.postalCode}} {{job.dateStart}} {{job.timeStart}} {{job.dateEnd}} {{job.timeEnd}} {{job.jobType}} {{job.jobSource}} {{job.company}} {{job.status}} {{job.notes}}
Lead (lead triggers only):
{{lead.id}} {{lead.fullName}} {{lead.firstName}} {{lead.lastName}} {{lead.phone}} {{lead.email}} {{lead.zip}} {{lead.serviceType}} {{lead.callTiming}} {{lead.urgency}} {{lead.source}} {{lead.status}} {{lead.notes}} {{lead.campaign}} {{lead.adName}}
Client (works on both — points to whoever the job/lead is for):
{{client.firstName}} {{client.lastName}} {{client.fullName}} {{client.phone}} {{client.email}} {{client.address}}
Contractor (job triggers only): {{contractor.name}} {{contractor.phone}} {{contractor.email}}
Office: {{office.name}}
Recipient kinds
- Assigned Contractor — uses the job's contractor (job triggers only). Skipped if no contractor.
- Client — uses the customer's phone (SMS) or email (Email).
- Static address — explicit phone or email entered into the rule. Useful for "always SMS our oncall at +1305..."
🌙 Quiet hours & pause-all
Two safety nets to stop automations from misbehaving.
Per-rule quiet hours
Each rule has a "Respect quiet hours" checkbox. When on, the rule only fires between 9am–9pm in the recipient's local timezone (derived from the job's postal code or the lead's stored timezone).
- Event-driven rules outside quiet hours log skipped: outside quiet hours and never fire.
- Time-based rules outside quiet hours skip the current 5-min tick WITHOUT stamping idempotency, so they retry once business hours open.
- The list view shows a 🌙 QH badge on rules with this setting.
Global pause-all
Big "Pause All" button at the top of the Automations panel. Click → confirmation → every fire path short-circuits. A red banner appears: "⏸ Automations are PAUSED — no rules will fire until you resume." Click ▶ Resume All to flip back. Use it as a kill-switch when something's wrong.
📜 Log & troubleshooting
Click 📜 View Log at the top of the Automations panel. Last 200 events.
Each row shows
- Status icon: ✓ sent · ✗ failed · ○ skipped
- Rule name + trigger kind + channel
- Recipient phone or email
- Linked job (display_id) or lead (name)
- Error message (red, when failed)
- Rendered preview (the actual text/email sent)
- Timestamp
Filters
Top of the modal: search box (matches recipient / preview / error), status dropdown, channel dropdown, per-rule dropdown, Reset button. Search is debounced 250ms.
Common failures
- "No CTM number configured for office X" — set one in CP → Office Settings.
- "SMTP not fully configured for office X" — fill in From email + host + port + app password.
- "Recipient ... has opted out of SMS" — they replied STOP. Manually call/email them instead.
- "contractor has no phone" — the rule targets the assigned contractor but their phone field is empty in the Contractors page.
⚙️ Office Settings
Owner CP → Automations & Integrations → ⚙️ Office Settings. Per-office configuration: CTM number for SMS, SMTP credentials for email, and the Jobs feature toggle.
🔧 Jobs Feature toggle
Each office card has an "Enabled" checkbox at the top. When unchecked:
- Users assigned only to that office can't open
/jobs,/contractors, or/service-areas— they redirect to/leads. - Top-nav links to those pages hide for them.
- The Reports modal hides the Jobs tab for them.
- The office's existing jobs are excluded from
/api/jobsqueries — the dashboard treats them as if they didn't exist (rows preserved in the DB; flipping back on restores everything). - Converting a lead in that office keeps the legacy "🔧 Mark as Opened in Workiz" reminder. When the toggle is ON, converting instead opens a pre-filled New Job form in the CRM — see Converting a lead.
The header line of each card shows the live status: "🔧 Jobs enabled" / "🔧 Jobs disabled."
CTM number
Each office picks ONE CTM number from the synced list. Used as the from for any SMS automation scoped to that office. The dropdown is searchable — type to filter by phone, name, or queue.
SMTP credentials
- From Email — the SMTP login email (also used as the From address by default)
- From Name — display name shown to recipients
- SMTP Host — e.g.
smtp.gmail.com - Port — 587 (STARTTLS) or 465 (SSL)
- App Password — Gmail and Outlook block plain passwords; you must generate an "app password" via account → 2FA → app passwords. Custom-domain SMTP works with the regular mailbox password.
Passwords are encrypted at rest with AES-256-GCM. The browser never sees the stored value back; an "✓ already set — leave blank to keep" hint replaces the field on edit.
📨 Send Test
Once configured, the Send Test button prompts for a recipient and pings them through the saved creds. Verifies host/port/auth before relying on the rule for real sends.
🕐 Calling hours
Owner CP → General Settings → 🕐 Calling Hours. Per-state windows defining when CSRs are allowed to call.
What it controls
- Lead detail panel — shows a "outside calling hours" warning when the lead's local time is outside the window for their state.
- Quiet-hours fallback — automations with "Respect quiet hours" use 9am–9pm local time as the default, but a future enhancement could honor per-state hours from this table.
Each state has start/end hours (24h, integer) plus optional Sunday hours.
📥 Lead sources
Owner CP → General Settings → 📥 Lead Sources. The list of channels leads can come from.
Each source has:
- Key — internal slug (e.g.
facebook,yelp). Match against incoming webhook payloads. - Label — display name (e.g. "Facebook Ads"). Shown in dropdowns and pills.
- Default Office — the office an inbound webhook lead lands in when the payload carries no office field (e.g. Angi never sends one). "General (default)" means it falls back to General. See the office-resolution order below.
- Color — pill color for the dashboard.
- Active — toggle to hide a source from new submissions without deleting it.
- Webhook token — auto-generated. Append to
/webhook/s/<token>for source-specific ingestion (forces the source key, ignores body's source field).
Reordering
Drag the ⠿ grip on the left of any row to reorder the list. The order you set drives how sources appear everywhere — filter pills and source dropdowns, not just this page. New sources land at the bottom.
How an inbound lead's office is chosen
For webhook leads with no explicit office, the office is resolved in this order:
- An
officefield in the payload (wins if present). - CTM Tracking Source match — the lead's tracking-source label (BookBack's
bb_source, etc.) matched against the "Tracking Source" column in CP → CTM Numbers; the matched line's office is inherited (exact, case-insensitive match). - The source's Default Office (this page).
- General as the final fallback.
A successful CTM match writes a "🏢 Office auto-set…" note to the lead timeline.
Built-in sources
facebook · google · lsa · yelp · angi · thumbtack · website · manual · nextdoor · bookback · online_booking. Add more from the CP — same flow.
AI-agent sources
Two special sources are used by the SMS / Meta AI agents when they create a lead via book_appointment or book_consultation_call:
smsagent— lead created by an SMS AI Agent. Office is inherited from the CTM line the conversation ran on.metaagent— lead created by a Meta AI Agent. Office is inherited from the Meta account.
You can rename / recolor these in CP → Lead Sources like any other source. Don't delete them — booking tools reference these keys directly.
🔗 Outbound webhooks
Owner CP → Automations & Integrations → 🔗 Webhooks. Three tabs: 🔌 Endpoints (configure URLs), 📊 Dashboard, and 📜 Timeline. On Endpoints, each event type has its own section with a main URL (always fires) plus optional filtered fan-outs — extra endpoints that only fire when their rules match.
📊 Dashboard — for a chosen window (Today / 7 Days / 30 Days / All Time): how many times each webhook event fired, and how many deliveries each destination URL received (with failure counts). 📜 Timeline — the most recent 100 deliveries, newest first: which event fired, success/HTTP status, the destination URL, and an expandable view of the exact data sent. The delivery log is kept for 30 days.
| Slot | Fires when |
|---|---|
| Jobs Webhook URL (Workiz format) | Job created (Submitted) only |
| Done Jobs Webhook URL | Job → Done |
| Deposit Jobs Webhook URL | Job → Deposit |
| Cancelled Jobs Webhook URL | Job → Cancelled |
| Finance-Cancelled Jobs Webhook URL | Finance Admin cancels a Done/Deposit job (job's main status stays unchanged) |
| Expected Completion Changed (filtered fan-outs only) | The deposit Expected Completion Date is set or edited (initial deposit or any later adjustment). Use {{ExpectedCompletionDate}} in the data map. |
| New Lead Webhook URL | Any new lead — manual / bulk / inbound webhook / online booking |
| Cancelled Leads Webhook URL | Lead → Irrelevant |
Plus a separate BookBack Jobs Webhook URL (CP → BookBack AI) that fires on job creation only. All payloads are Workiz-shape so a single Zap can consume any of them.
Filtered fan-outs
Under each event section you can add additional endpoints that receive the same payload but only when their rules match. Available filter dimensions:
- Job events (created / done / deposit / cancelled / finance-cancelled): Office, Partner, Job Type.
- Lead events (created / cancelled): Office, Source, Partner.
Within a dimension, multiple selected values are OR'd (any match). Across dimensions, conditions are AND'd (all must match). Leaving a dimension empty means "any value." Each fan-out has its own Enabled toggle, so you can pause one without deleting it.
Custom request builder (filtered fan-outs)
Each filtered fan-out has a Zapier-style request builder so you control exactly what the endpoint receives:
- Payload Type — JSON (default), Form (url-encoded), XML, or Raw.
- Data — key/value rows. The value can be static text, a
{{Variable}}from the CRM (use the + var picker), or a mix. Leave Data empty to send the full standard Workiz-shape payload as-is. For Raw, you type the whole body and insert variables inline. - Basic Auth (optional) —
username:password, encoded into anAuthorization: Basicheader. - Headers (optional) — extra request headers; values can use
{{Variable}}too.
Main per-event URLs always send the standard payload; the custom builder applies only to filtered fan-outs. (We dropped Wrap-in-Array / Unflatten / file upload — sensible defaults are used.)
Payload includes
UUID, SerialId, JobDateTime, JobEndDateTime, CreatedDate, Status, Phone, Email, FirstName, LastName, Company, Address, City, State, PostalCode, Country, Unit, Latitude, Longitude, JobType, JobSource, ServiceArea, Tags, Team (the contractor's NAME only — never private contact info), AreaCode, OfficeAddress, DepositAmount, DisplayId. Plus a few lead-only extras when fired from the leads side (LeadId, Office, Urgency, Campaign, FbLeadId, IrrelevantReason). Finance-cancel events add FinanceCancelReason, FinanceCancelNote, FinanceCancelledAt, FinanceCancelledBy.
Fire-and-forget
Webhook calls don't block the API response. Failures log to the server console but don't bubble to the user. If a downstream URL is offline, the event is lost — there's no retry queue (yet). Sub-webhook lookup failures never block the main URL.
📥 Inbound lead webhooks
Several endpoints accept inbound leads. Each has slightly different auth.
The endpoints
POST /webhook/leads— generic. Requiresx-webhook-secretheader (or?secret=). Source comes from the body'ssourcefield, fallback to?source=, defaultmanual.POST /webhook/facebook— legacy alias. Same auth as above; source forced tofacebook.POST /webhook/s/:token— source-token-protected. Token comes from the source row in the CP. The source key for the lead is forced from the token (body's source field is ignored).
Body fields recognized
The handler accepts many common shapes:
full_name,fullName, ornameemailphone_numberorphonezip_codeorziplead_idorleadId(stored as fb_lead_id; not used for dedup)ad_name/adNameadset_name/campaign/campaign_namepartner_nameoffice— optional. When omitted, the office is resolved by CTM Tracking Source → source Default Office →General(see Lead sources for the full order).bb_source/tracking_source/ctm_source— tracking-source label, matched against CP → CTM Numbers to auto-assign an office.fee— per-lead acquisition cost (Angi). Stored on the lead and shown as a 💵 pill beside the source badge.what_can_we_do_for_you_today/service_type(Angi:taskName)how_soon_do_you_need_the_service/how_sooncreated_time(defaults to now)extra_notes/message/notes
Office auto-assignment
Inbound leads that don't carry an office are routed automatically: the lead's tracking source (bb_source etc.) is matched against the "Tracking Source" column in CP → CTM Numbers and the matched line's office is inherited; otherwise the source's Default Office applies; otherwise General. A match writes a "🏢 Office auto-set…" note to the timeline.
Rate limit
100 leads per source per 10-minute window. Exceeding returns 429 to the source. Tracked in memory per server process.
Inbound responses lead with { "status": "success" } so partners like Angi acknowledge delivery and don't retry/duplicate.
BookBack reactivation
When source = bookback, the handler matches the incoming phone against existing leads. On match: reactivates the existing lead (resets to Fresh, appends notes, records a bookback_calls row) instead of creating a new one. See the BookBack chapter.
📊 Reports
Topbar → 📊 Reports. Wide modal with two tabs (📥 Leads · 🔧 Jobs) and four period buttons (Today / This Week / This Month / Custom) plus an office multi-select. The Jobs tab is hidden when the user has no jobs-enabled office.
Leads tab — what's measured
- KPI tiles: Total Leads · Converted · Irrelevant · Conversion Rate · Fresh · Not Yet Scheduled · Needs Follow Up · Avg Response Time
- Breakdown bars: Leads by Status · by Source · by Office
- Lead status by day — stacked daily chart (Converted / Open / Irrelevant)
- BookBack AI — daily performance — Detected / Reactivated / Converted bars
- Follow-up activity — call attempts vs same-day conversions on past leads
- Conversion efficiency — total leads vs converted, broken down by attempt # (1st call, 2nd call, …)
- Agent performance table: Calls · Claims · Conversions · Avg Response Time
Jobs tab — what's measured
- KPI tiles: Total Jobs · Completed (with done %) · Cancelled (with cancel %) · Revenue (sum of closing_total on Done) · Deposits · Rebooked · K-Accepted · Parts Cost
- Breakdown bars: Jobs by Status · by Office · by Type · by Source
- Daily jobs created — stacked daily chart (Done / Open / Cancelled)
- Top contractors — table with Jobs · Done · Cancelled · Revenue per contractor
Auto-emailed reports
Owner CP → General Settings has the Email Settings panel. Configure recipients + tick which periods (daily / weekly / monthly) should auto-send. Schedule (EST):
- Daily — every night at 11 PM EST
- Weekly — every Sunday at 11 PM EST
- Monthly — 1st of the month at 11 PM EST
Email content mirrors the on-screen report — KPIs, breakdown bars, inline SVG charts, agent performance, plus the full Jobs section. Date windows are EST-aligned (a previous bug was clipping daily reports to ~3 hours of data — fixed).
👥 Users & permissions
CP → 👥 Users. Only the owner can create, edit, or delete users (and reset another user's password).
Per-user fields
- Name + email
- Role — viewer / agent / admin / finance_admin / owner. (
partneris provisioned separately, from the Job Partners screen, as an external sandboxed login.) - Office — comma-separated list (
Sliding,Garage) orallfor owner-equivalent visibility - Active — uncheck to disable login without deleting the user
- Password — set a new one on edit (never displayed back)
What roles can do (recap)
See the Login & roles chapter for the full matrix. Tl;dr: owner does everything (incl. CP, Users, deletes); finance_admin = admin powers + the Finance dashboard + edit Contractors/Areas; admin runs day-to-day lead/job write actions incl. archive; agent works leads + reads jobs/contractors/areas; viewer reads everything (and can edit Finance fields); partner is a read-only external login scoped to its own records. Page visibility is broad; write capability is gated by API role checks.
📋 Audit log
Topbar → 📋 Audit Log. Owner-only. Every system event recorded chronologically.
What's recorded
- New lead inserts (per source)
- Lead status changes
- Job creates / status changes / rebooks
- BookBack reactivations
- Webhook rate-limit hits
- Office adds / deletes
- Calling-hours updates
- Lead archive / restore / delete
Each row carries the actor (user name or "System"), the office (or "all"), a short text description, and a JSON metadata blob with structured details. Filter by type and date range.
⏱ Global timeline
Topbar → 📋 Timeline. Admin / viewer / owner. The fast-scrolling activity feed across every lead.
Each entry: timestamp + lead name + entry type (status / call / note / sys) + body. Use it to "what's been happening" at a glance without opening individual leads. Scroll back as far as you like — paginated infinitely.
Filters
- Office (multi-select)
- Type (Status changes / Calls / Notes / System)
- Date range (preset or custom)
- User (who performed the action)
⚙️ Other CP settings
A grab-bag of smaller owner-only settings. Reach them from CP landing → the relevant tile.
Job Settings
- 🛠 Job Types — values for the New Job + Contractor dropdowns. Each has a label + sort order.
- 📊 Job Partners — operational partners (GDQ, Nehoray, Raful, …). Each row gets a webhook URL; the partner is auto-tagged from which URL was hit.
- ⛔ Cancel Reasons — pre-canned reasons for Cancelled jobs.
- 🏷 Job Tags — color-coded tag definitions used in the job detail panel.
Numbers
- 📞 CTM Numbers — synced from CTM via API. Each row can be assigned an Office, an SMS AI Agent (the 🤖 column — dropdown picks from CP → 💬 SMS AI Agents), and toggled "no-reply" (disables manual + AI replies on that number).
- 🔵 LSA Accounts — Local Services Ads account directory (LSA customer_id → office mapping).
- 💬 SMS Templates — quick-reply text templates surfaced in the SMS panel's 📋 button.
Automations & Integrations
- 📲 Office Settings — covered above.
- 🤖 Automations — covered above.
- 🔗 Webhooks — covered above.
- 📬 Meta Accounts — FB Pages + IG Business accounts that feed the Meta inbox. Owner adds each Page's long-lived access token + office assignment. See Meta inbox.
- 🤖 Meta AI Agents — per-Page AI auto-reply bots. Persona, status, model, auto-send, handoff caps, cost counter per Page. See Meta AI Agents.
- 💬 SMS AI Agents — reusable SMS AI personas. Create once, assign to many CTM numbers. Booking tools, follow-up scheduler, dry-run + auto-send, cost counter. See SMS AI Agents.
- 🤖 AI Assistant — model, daily budget per role, audit log replay. See AI Assistant chat.
- ⚡ BookBack AI — single URL field for the BookBack Jobs Webhook (fires on Submitted only).
- 🌐 Online Booking Tool — covered above.
- 🌐 Google Places API — single field for the Google Places API key. Used by the address autocomplete on New Job + the bulk Service Areas geocoder + the in-app Google Maps panels. Restrict the key by HTTP referrer in Google Cloud Console.
App Settings
- Escalation Timer (minutes) — how long a Fresh lead can sit unclaimed before it counts as escalated in reports.