The CRM turns a WebProCMS site into a lead hub. Every form submission, AI-chat follow-up request, and member signup can become a tracked contact with a status, groups, and a full interaction timeline. Staff can add, edit, import, export, and email contacts from the dashboard — and the same contact record is the attach point for future features (Newsletters, Online Booking, Donations, Tickets) so a lead captured today is reachable by whatever the site adds tomorrow.
The problem
A marketing site collects leads in a dozen disconnected ways: a contact form emails the owner, a chat widget transcript sits in another table, a member signs up in Stripe, someone's business card gets typed into a spreadsheet. None of them talk to each other. The owner can't answer "who has reached out, where did they come from, and what have we said to them?" without stitching four tools together by hand — and when they later add a newsletter or a booking system, none of those leads are in a list it can use.
The usual fix is to bolt on Salesforce / HubSpot / Mailchimp. That means a second login, a second bill, an export/import dance to get the site's own form data into the external system, and a privacy surface (every lead's data leaving for a third party) that a lot of small businesses don't want.
The fix
A self-contained feature module under app/Features/Crm/ gives the site its own contact database, living in the install's own tables. Public form submissions and AI-chat follow-up requests flow into it automatically; members get linked on signup; staff manage everything from Dashboard → CRM. The data never leaves the install.
Like every addon, the CRM is structured as a feature module and gated by feature:crm middleware on every dashboard route. The service provider boots unconditionally (loading migrations, views, and routes), and the routes 404 + the sidebar entry hides when the feature is off — the data stays in place. Because it's default ON for members, the schema materializes through the normal php artisan migrate path on any member install without the admin having to toggle anything.
When to turn it on
The CRM is on by default for members. Leave it on if you want to:
- Capture leads from your own forms without an external CRM — every contact form that has an email field can create a contact automatically.
- Track the relationship — log calls, notes, status changes, and sent emails on a per-contact timeline.
- Segment contacts into groups for follow-up and (when Newsletters ships) bulk email.
- See where a lead came from — connect a contact to the built-in Analytics visitor record (first/last touch, landing page, UTM campaign).
- Tie members to contacts — a paying member and their CRM contact are the same person, linked.
- Import an existing contact list from a CSV, and export your contacts at any time.
Turn it off (admin → Settings → Features) if the site is purely informational and you don't want to track who reaches out. Toggling off hides the UI and 404s the routes; it never drops the tables.
How it works
Five pieces, all reachable from Dashboard → CRM:
- Contacts — the people. A list with search + status/group filters + configurable columns, a create/edit form, and a detail page with the full timeline.
- Modules — Deals, Companies and Tasks are optional halves of the CRM, each switched on or off in Settings → Modules (see below). Contacts and the timeline are always on.
- Capture hooks — forms, the AI chat bot, and member signup feed contacts in automatically (each independently toggleable).
- Settings (admin only) — pick which modules you use, define custom statuses, manage groups, choose table columns, and flip the three capture integrations on/off.
- The cross-feature API — a single
CrmContactssupport class other features call to create contacts and log interactions, so the CRM never has to know about them.
Contacts
A contact carries: name, email (unique, stored lowercased — multiple email-less contacts are allowed), phone, company, notes, a source, a status, and any number of groups.
Source records how the contact entered the CRM and is shown as a label on the list and detail pages:
| Source | Set when |
|---|---|
| Manual | Created by hand in the dashboard |
| Form | Captured from a public form submission |
| Chat Bot | Captured by the AI chat bot's follow-up tool |
| Import | Created by a CSV import |
| Member | Created when someone registered a member account |
| Store | Captured from an Ecommerce order |
Contacts are added/edited/deleted from Dashboard → CRM. Creating a contact requires at least a name or an email. The edit screen logs a timeline entry whenever the status changes.
Beyond the basics, every contact also carries a job title, a lifecycle stage (Lead / Customer — distinct from the pipeline status), an owner (the staff member responsible — filterable via the "My Contacts" pill), a LinkedIn/social URL, a full address block, a denormalized last-contacted timestamp (bumped by real touchpoints: notes, calls, meetings, emails, form/chat captures, orders), and any custom fields the admin defines. Companies similarly carry industry, employee-count band, owner, and an address. Captured contacts can be auto-assigned an owner (a fixed default or round-robin across managers) from Settings → Integrations.
The detail page
Each contact's detail page (/dashboard/crm/{id}) shows:
- A header with the name, current status badge, source, and date added.
- A details card (email, phone, company, groups, notes).
- A Lead Source card — the first-touch origin/campaign/referrer/landing page, plus a Registered from row: the country (flag + name, resolved at capture time from the CDN geo header or the local geo database) and the IP address the contact was first captured from, for judging whether a lead is legit. Stamped by visitor-facing capture paths (forms, member registration, real-estate leads); manual creation and imports leave it blank, and contacts captured before this shipped have no IP on record.
- An add-note box and the full timeline (paginated, newest first, with an icon per entry type).
- A Send Email action (see below).
- A Visitor Analytics panel — only when the Analytics addon is on and the contact has a visitor hash.
- A Membership panel — only when the Memberships addon is on.
Modules — using only the half you need
Not every business runs a sales pipeline. A walk-in clinic, a restaurant, a salon — anyone whose customers just call and book — needs the CRM's first half (who reached out, and what we said back) and none of its second (quoted work moving through stages, company accounts, a follow-up queue). Left on, that second half is clutter in front of the client.
Settings → Modules switches three of them independently:
| Module | What it covers | Off means |
|---|---|---|
| Deals | The pipeline, its stages, and everything priced | No Deals nav or board, no deals panel on contacts or companies, no Pipeline settings tab, no store orders → won deals integration, and the reports drop their money sections |
| Companies | Company accounts contacts belong to | No Companies nav or pages, no Company field on contacts or deals, no Company column or bulk-assign on the contacts list |
| Tasks | Follow-up reminders, their emails, and the digest | No Tasks nav or queue, no Tasks card on contacts/companies/deals, and the reminder and digest emails stop |
All three default on, so an install that never visits this page keeps the surface it already had.
Off hides; it never deletes. Switching Deals off leaves every deal, stage
and pipeline exactly where it was — the settings page says so beside each
toggle, with a live count of what is currently hidden. Switch it back on and
everything reappears, including saved list-column choices. The one thing that
genuinely stops rather than hides is the Tasks email: a reminder for work
nobody can see would be noise, so crm:send-task-reminders and
crm:send-task-digests no-op while the module is off.
Why this isn't a "type of site" setting. The obvious design is to ask what industry the business is in and preset the modules from that. It's the wrong axis. What actually decides whether Deals earn their place is does this business quote work before doing it — and that cuts across verticals rather than following them. The same urgent care with no pipeline for walk-ins runs a real one for employer occupational-health contracts, where each employer is a Company and each contract is a Deal. An industry preset would guess, and when it guessed wrong the owner would see a feature they'd read about with no way to reach it. A switch in Settings can always be overruled.
Existing automation rules are handled conservatively: a rule already using a deal trigger keeps that option visible in its dropdown even with Deals off, so re-saving the automations list can't silently rewrite a rule you didn't touch.
Deals pipeline
Optional — switched off in Settings → Modules by installs that don't quote work.
Dashboard → CRM → Deals is a kanban board: columns are admin-definable stages (each with a win probability, managed in Settings → Pipeline; Won and Lost are fixed outcomes), cards are deals (title, value, currency, linked contact/company, owner, expected close date), and dragging a card changes its stage — logged to the deal's own timeline. Cards can also be reordered within a column: dropping a card onto another card slots it in just above that card (dropping on empty column space appends to the end), and the order persists (deals.sort_order). Dropping a card on the Won zone stamps won_at; the Lost zone asks for an optional lost reason. A list view (with configurable columns) is one click away, and each deal has a detail page mirroring the contact page: details panel, quick stage switcher, Mark Won / Mark Lost / Reopen, tasks, notes, and a timeline. Deals appear as panels on the contact and company detail pages. With Ecommerce on, an optional integration records every completed store order as a won deal so store revenue shows up in the pipeline.
Multiple pipelines
Installs that run more than one process (sales, partnerships, fundraising…) can add additional pipelines from Settings → Pipeline: each new pipeline starts with the standard stage ladder (Qualified / Contact Made / Proposal Sent / Negotiation + Won/Lost) and gets its own stage editor. The deals board grows a pipeline switcher (shown only when more than one exists; the choice rides in the URL as ?pipeline=), the deal create/edit forms grow a pipeline picker, and moving a deal to another pipeline re-anchors it on that pipeline's ladder (the picked stage when valid, otherwise the first open stage). The first pipeline is the primary one — automatic deals (store orders, integrations) always land there — and a pipeline that still holds deals (or the last remaining pipeline) can't be deleted.
Tasks & follow-ups
Optional — switched off in Settings → Modules, which also stops the reminder and digest emails.
Dashboard → CRM → Tasks is the team's follow-up queue, bucketed into Overdue / Today / Upcoming / No due date with a recently-completed list. A task has a title, a type (to-do, call, email, meeting), a due date, an owner, and can hang off a contact, company, and/or deal — each of those detail pages embeds a Tasks card with inline add/complete. The sidebar shows a red overdue badge, and a daily digest email sends each owner their due and overdue tasks. The contact page's note box also logs calls and meetings with an optional duration and outcome.
Custom fields
Settings → Custom Fields lets an admin add fields to contacts, companies, and deals — text, textarea, number, date, select, or checkbox. They render on the create/edit forms and detail pages, are available as table columns, round-trip through CSV import/export, and are searchable. Values live in a custom_data JSON column per record; definitions live in one Setting per entity, so no schema change is ever needed.
Saved views & bulk actions
Any combination of search + filters on the contact list can be saved as a named view (per-user), surfaced as tabs above the table. Checkboxes on the list open a bulk bar: assign a status, add to a group, set an owner or company, delete — or Send Newsletter, which snapshots the selection into a CRM group and opens a pre-targeted newsletter draft. The Newsletters audience picker also offers your saved views directly (CRM view: …) as a live audience that re-evaluates the filters at send time.
Duplicate detection & merge
The contact detail page shows a possible duplicates banner (same name with a different email, or same phone) with a review-and-merge modal: pick the survivor, and groups, deals, tasks, and the full timeline are combined; blank fields on the survivor fill in from the duplicate; the merge is logged as a timeline entry. Settings → Duplicates has a site-wide scan covering contacts and punctuation-variant company names ("Acme Inc" vs "Acme, Inc.").
Reports
Dashboard → CRM → Reports: open pipeline value by stage, won/lost totals and win rate for the current month, contacts by status and by source, a team leaderboard (contacts owned, open pipeline, won value this month), and a going cold list of contacts untouched for 30+ days.
With the Deals module off, the money sections drop out (pipeline by stage, the won/lost/win-rate strip, and the leaderboard's two value columns) and the page keeps what still applies: contacts by status, contacts by source, leads by source, campaigns, the leaderboard's contact counts, and going cold.
Automations
Settings → Automations holds small "when this, do that" rules. Actions are create a task, assign an owner, add to a group, send an email (with a :name placeholder), and — when Marketing is on — enroll in a marketing sequence. Event rules run instantly; the inactivity rule is swept daily and fires at most once per cold spell. Every run is fire-and-forget and leaves an automation entry on the contact's timeline.
Triggers, each with its own optional scoping:
| Trigger | Scope | Fires when |
|---|---|---|
| Contact is captured | a source, or any | A contact is created — or re-captured from that source, so a returning customer re-enters a source-specific rule |
| Contact status changes | a status, or any | The status is changed on the edit screen |
| Deal moves to a stage | a stage, or any | A deal is dragged or moved into that stage |
| Deal is won | — | A deal reaches the won stage |
| Contact inactive for N days | days | The daily sweep finds a contact untouched that long |
| Enrolls in / completes a course | a course, or any | Courses records the enrollment or the last lesson |
| Places a store order | minimum amount | A shop order is paid (once, whichever of the webhook / return page wins) |
| Makes a donation | minimum amount | A gift settles — one-time or a recurring plan's renewal |
| Pays an invoice | minimum amount | An invoice reaches paid in full, by card or an offline payment keyed in |
| Books an appointment | a service, or any | A booking is confirmed. Cancels, reschedules and no-shows don't re-fire it |
| Buys event tickets | an event, or any | A ticket order completes, free or paid |
The commerce triggers scope on whichever axis the purchase actually has: a booking is for exactly one service and a ticket order for exactly one event, so those scope by record; an order, a gift and an invoice each cover many things, so those scope by how much money changed hands. A minimum of 0 (or blank) means any amount, and the comparison ignores currency — it reads as "this many units of whatever the sale was in". Each trigger only appears in the dropdown when its feature is enabled.
Custom statuses
Statuses are the lifecycle stages a contact moves through, and they're admin-definable — not hardcoded. Each status has a name, a colour (a Flux badge token like blue / amber / green), a sort order, and a default flag. New contacts get the default status.
Every install seeds five sensible defaults in the migration: New (default), Contacted, Qualified, Customer, Archived. Admins reorder, rename, recolour, add, or remove them from Settings → Statuses. Deleting a status moves its contacts to the default (at least one status is always required).
Statuses are filterable on the contact list and render as a coloured badge throughout.
Groups
Groups are flat, free-form tags for segmentation — a contact can be in many groups. They're the list mechanism the future Newsletters feature will broadcast to. Create them inline on the contact form, in Settings → Groups, or on the fly during CSV import. Group filtering is available on the contact list.
Interaction timeline
Every meaningful touch is logged as an interaction on the contact, stored in a dedicated crm_interactions table (not the audit Activity Log — these are user-facing CRM records that survive log-retention cleanup and cascade-delete with their contact). Entry types:
| Type | Logged when |
|---|---|
| Note | Staff adds a note on the detail page |
| Email sent | Staff sends a one-to-one email, or BCCs the CRM inbound address on an email from their own inbox |
| Email opened | A tracked one-to-one email's open pixel loads for the first time |
| Form submission | A form creates/updates the contact (carries the full submission payload) |
| Chat bot | The AI chat tool captures a follow-up (carries the conversation reference) |
| Status change | The contact's status is changed on the edit screen |
| Member linked | The contact is linked to a member account |
| Imported | The contact was created by a CSV import |
Each interaction records the acting user (the "causer"), a subject, an optional body, and a JSON meta payload. It also has a generic related_type / related_id morph pointer, which is how a form submission links to its FormSubmission row and a chat capture links to its conversation — and the same pointer is how future features attach their own records (a booking, a donation, a ticket) with no schema change.
Form-submission capture
Every form in the form builder gains an "Add submitter to CRM contacts" switch (default on, shown only when the CRM is enabled). When a form is submitted and:
- the CRM feature is on, and
- the site-wide Capture form submissions integration is on, and
- the form's own switch is on, and
- the submission contains an email field,
…the CRM creates or updates a contact (matched by email, source Form), best-effort filling name and phone from the submission, and logs a form-submission interaction carrying the full payload. If the form also saves submissions to the database, the interaction is linked to that submission row.
The hook is fully isolated — wrapped so that any CRM error is reported and swallowed. A CRM problem can never break a public form submission. It also no-ops cleanly when the CRM is disabled, when the form opts out, or when there's no email field. Capture happens even when "Save submissions to database" is off, because the contact and timeline are the system of record either way.
One-to-one email
The contact detail page has a Send Email action (shown when the contact has an email): a subject + message, sent to the contact's address and logged as an email-sent interaction with the acting staff member recorded. The body is plain text rendered with line breaks, opening with a "Hi {name}," greeting.
This is deliberately one-to-one — bulk email lives with the Marketing feature, which broadcasts to CRM groups. There's no two-way threading: a reply is an ordinary email, and where it lands depends on which mailbox the message left.
Sending from your own mailbox
By default the message goes out on the site's shared mail settings, from the site's From address — so the contact's reply goes back to the site, not to the person who wrote to them. On a team that is usually the wrong outcome.
Each user can instead store their own SMTP details under Settings → Sending Email (their own account settings, not the site-wide Dashboard → Settings → Email page). Then their contact emails leave their real mailbox: signed by their own email provider, aligned with a domain that provider is authorised to send for, and the reply arrives in their inbox. Anyone who hasn't set one up falls through to the site mailer exactly as before.
- Use an app password, not an account password. Providers refuse account passwords for SMTP once two-factor is on, and an app password is scoped to sending and revocable on its own.
- A Super can set a mailbox up for someone else from the row menu on Dashboard → Users, so a team member never has to log in to do it. Reaching your own only needs access to that page; reaching anyone else's requires Super. The stored password is write-only on both screens — it is never sent back to the browser, an empty field means "leave it alone", and removing it is a separate action.
- Send test to me proves the credentials before a real contact is on the receiving end. SMTP failures otherwise surface at send time, not when the form is saved.
- Keep a copy in my inbox (on by default) BCCs the sender. SMTP submission files nothing in a Sent folder — a mail client does that itself over IMAP — so without it the sender keeps no copy of what they sent. The copy also threads with the contact's reply.
- Open tracking is skipped on this path. A tracking pixel inside a message that presents itself as an ordinary personal email is both a spam signal and a broken promise, so the pixel is only added to sends that go out through the shared mailer.
Open tracking
When Settings → Integrations → Track email opens is on (the default), each one-to-one send embeds an invisible pixel backed by a per-send token row (crm_email_opens, mirroring Marketing's campaign tracking). The first time the pixel loads, the send is stamped as opened and an Email opened entry — "Opened: {subject}" — lands on the contact's timeline; repeat opens only bump a counter, so the timeline never floods. The pixel endpoint (crm/e/{token}) always returns the same GIF, so the URL leaks nothing about which tokens exist. Turn the toggle off and emails go out with no pixel and no tracking row.
BCC to CRM
Staff who email contacts from their own inbox (Gmail, Outlook, …) can log those emails automatically: create an inbound address with an email provider (e.g. Postmark inbound), paste it into Settings → Integrations → BCC to CRM, generate the webhook secret, and point the provider's inbound webhook at the shown URL (crm/inbound/{secret} — same URL-path-secret pattern as the Ticket System's reply-by-email). Then BCC that address on any outbound email: the provider posts the message to the webhook, the sender is matched to a dashboard user by email (anything else is ignored), every To/Cc recipient is matched to a contact, and each match gets an email-sent interaction (subject + body, marked via: bcc, bumping last-contacted). Misses are acknowledged with a 200 and an ignored status so the provider never retries a permanently unloggable payload; a per-contact rate limit keeps a runaway auto-forwarder from flooding a timeline.
Lead scoring
With the Analytics addon on, contacts that carry a visitor hash get an automatic lead score (0–100) computed from their actual site activity:
| Signal | Source | Max |
|---|---|---|
| Visit recency | visitor_attributions.last_seen_at (≤2 days: 30, ≤7: 20, ≤30: 10) |
30 |
| Active days in the last 7 | page_view_visitors (10 per active day) |
30 |
| Conversions in the last 90 days | conversions (20 for the first, +10 each) |
40 |
Scores land on the contact record via the daily crm:refresh-lead-scores LazyCron sweep (batched — three aggregate queries per 500 contacts, never a live scan per row), and the contact detail page refreshes its own row on view, showing a Lead Score card with the number, a Hot/Warm/Cool badge (≥70 / ≥40 / below), and the per-signal breakdown. On the contact list, Lead Score is available as a badge column, a sort option ("Lead Score" floats scored contacts first), and a filter (Warm 40+ / Hot 70+) that also round-trips through saved views. Contacts with no visitor linkage simply show no score.
CSV import & export
Import (from the Import / Export menu on the contact list) accepts a CSV with the headers name, job_title, email, phone, company, notes, status, lifecycle, owner, groups, linkedin, address, city, state, postal_code, country — plus any custom-field names — case- and space-insensitive, extra columns ignored. owner matches a dashboard user's email. Email is required per row; rows are deduped against existing contacts by email; invalid-email rows are skipped and counted. A status named in the row matches an existing status; multiple groups are separated by ; or | and created on demand. The modal also offers an optional bulk status and group to apply to every imported row. The file is streamed row-by-row, so even large lists import at constant memory. After import you see a summary: N added, N updated, N skipped.
Export streams the currently-filtered contacts to a CSV download (contacts-YYYY-MM-DD.csv) with every standard column (including job title, lifecycle, owner email, address fields, and last-contacted date) plus all custom fields.
AI chat-bot capture
When the AI Chat addon is enabled, the chat agent gains a save_contact_request tool — registered only when the CRM is on and the Capture chat bot follow-ups integration is on. The bot is instructed to offer it when a visitor wants a follow-up (a call back, a quote, more information): it collects the visitor's name and email (phone optional) conversationally, then calls the tool with a one-sentence reason.
The tool creates/updates a contact (source Chat Bot, validating the email) and logs a chat-bot interaction whose body is the reason and whose meta + morph pointer reference the chat conversation, so staff can open the transcript. Like every chat tool it never throws — an invalid email comes back as a polite error string asking the visitor to confirm their address.
Analytics linking
When the Analytics addon is on, a contact captured from a form or chat is stamped with the visitor's stable attribution hash (the same salted, cookieless hash Analytics uses to connect a Monday ad-click to a Wednesday form-fill — no IP, no raw identifier). On the detail page, a Visitor Analytics panel then shows that visitor's first/last seen times, first landing page, referrer, and first + latest UTM campaigns, read straight from the Analytics visitor_attributions table.
This is read-only and additive — the CRM never writes to or alters the analytics tables, and the panel simply hides when Analytics is off or the contact has no hash.
Memberships linking
When the Memberships addon is on:
- Registering a member (on the public register page or via an admin in the dashboard) creates a linked contact (source Member), gated by the Capture new members integration toggle.
- A contact's detail page shows a Membership panel that finds the matching member by stored link or email, displays their status, and offers Link / Unlink so staff can pin the association.
All of it is gated on the memberships feature key and no-ops when that feature is off.
The cross-feature API
The single surface other features call is App\Features\Crm\Support\CrmContacts:
CrmContacts::enabled(): bool
CrmContacts::createOrUpdate(array $attributes): ?CrmContact
CrmContacts::logInteraction(CrmContact|int|null $contact, string $type, ?string $subject, ?string $body, array $meta, ?Model $related, ?int $userId, CrmDeal|int|null $deal): ?CrmInteraction
CrmContacts::findByEmail(string $email): ?CrmContact
CrmContacts::eligibleOwners(): Collection // manager+ users for owner dropdowns
Deal lifecycle operations live in CrmDeals (moveToStage / markWon / markLost), merge in CrmMerge, saved views in CrmSavedViews, custom fields in CrmCustomFields, and the rule engine in CrmAutomations.
createOrUpdate matches by lowercased email, sets the default status and source on create, and on update fills only blank fields (never clobbering operator-entered data) while filling member_id / visitor_hash when they're currently null. Every method no-ops (returns null) when the CRM is disabled, and the write methods catch and report any error rather than propagating — the contract for caller hooks is "fire and forget," which is exactly why a CRM failure can't break a form, chat reply, or member signup.
Settings
Dashboard → CRM → Settings (admin only) has nine tabs (the landing tab is Modules; Pipeline hides when Deals are off):
- Modules — switch Deals, Companies and Tasks on or off for this install.
- Statuses — add / rename / recolour / reorder statuses and pick the default.
- Pipeline — manage pipelines (add / rename / delete; new ones seed the standard ladder) and each pipeline's stages with win probabilities (Won/Lost are fixed).
- Groups — add / delete groups (with a contact count each).
- Custom Fields — define extra fields for contacts, companies, and deals.
- Table Columns — choose the contact-list and deal-list columns and their order.
- Duplicates — site-wide duplicate scan + merge for contacts and companies.
- Automations — the when-this-do-that rules.
- Integrations — capture toggles (forms, chat bot, members), the store orders → won deals switch, track email opens, the BCC to CRM inbound mailbox (address + webhook secret), and contact auto-assignment (default owner or round-robin).
Roles & access
CRM dashboard pages (contacts list, create, edit, detail) require the Manager role or higher; Settings requires Admin. All routes additionally carry feature:crm, so they 404 when the feature is off, and the Deals, Companies and Tasks routes carry a second gate (EnsureCrmModule) that 404s them when their module is switched off. Both gates are enforced server-side rather than by hiding the link — a bulk "set company" call, for instance, is refused on the server, not just dropped from the toolbar. The only public-facing CRM routes are the two unauthenticated machine endpoints: the open-tracking pixel (crm/e/{token}, responds identically for unknown tokens) and the BCC inbound webhook (crm/inbound/{secret}, authenticated by the URL-path secret). Everything else captures through the existing form, chat, and membership flows.
What it's not
- Not a bulk-email tool. The CRM sends one-to-one emails; bulk broadcasts go through the Marketing feature, which targets CRM groups, statuses, and saved views directly.
- Not an inbound email inbox. Outbound emails are logged (sent from the dashboard or BCCed from your own inbox), but replies still go to the site's normal mailbox — there's no two-way email threading.
- Not a CSV column-mapper. Import expects fixed headers; a point-and-click column-mapping UI is a future enhancement.
- Not a contact form builder. Forms are built in the existing Forms & Submissions system; the CRM just consumes their submissions.