Deep Dive · How the whole thing works

One POST fires the entire funnel.

A complete, end-to-end tour of the Braven Lead Router — the routing brain, the data model, the capture pipeline, the CRM handoff, the tracking, and how every piece sits inside a standard WordPress install. Diagrams, worked examples, and real code.

WordPress + ElementorCustom PHP · CPTs · ACFGTM + GA4 Smrts CRM webhook~6KB CSS · ~7KB JS · no jQuery26 unit tests
00

The 60-second version

A visitor answers three questions. A scoring engine ranks their intent and picks the right next step. On submit, one request stores the lead, sends it to the CRM, emails the team and the prospect, and records a conversion in GA4 — with a browser event trail the whole way.

The visitor's path
1 · Who are you?City · County · Chamber · Foundation · Corporate
2 · Your goalAI · Digital Mktg · Social · Full program
3 · QualifiersFunding · Timeline · Role · Scale
4 · Routed pathBook · Proposal · Funding · Nurture
3

questions to a routed result

Buyer type, goal, and a short qualifier set — under a minute, mobile-first.

1

POST does everything

Persist + CRM + email + GA4 fire atomically on a single request.

0

page-builder bloat

Deep logic in custom PHP; assets load only where the tool is placed.

26

unit tests, no DB

The routing brain is framework-agnostic and tested without WordPress.

01

The problem it solves

Braven's institutional buyers — a city economic-development office, a county, a chamber, a foundation — are not the same buyer. They have different money, different timelines, and different definitions of success. A single generic “Contact Us” form treats them identically and leaks the ones who were ready to move.

Different buyers

A funded city ready this quarter needs a booking link. A foundation seeking a grant needs a funding conversation. One form can't serve both.

Different intent

“Just exploring” and “I approve the budget” should not land in the same bucket. Intent has to be measured and acted on.

Clean handoff

The lead has to arrive in the CRM already segmented and scored, with a paper trail — not as an undifferentiated email.

So the tool does three things a form can't: it self-segments the visitor, scores their intent transparently, and routes each one to the next step that actually fits — while capturing everything downstream systems need.

02

Big-picture architecture

One design decision shapes everything: the routing brain has no WordPress dependency. It's plain PHP that takes a selection and returns a decision. WordPress is a thin adapter layer wrapped around it. That's why the logic is trivial to test and reason about, and why the tool is honest to document.

Three layers, one direction of dependency
Front-end
Server-rendered wizard + router.js (state machine, focus, dataLayer). Talks to REST. Progressive enhancement — works before JS.
WP adapter
CPTs, taxonomies, custom table, ACF, REST routes, shortcode + Elementor widget, admin. Binds the core to WordPress primitives.
Pure core
Routing_Engine · Lead_Validator · Webhook_Dispatcher · GA4 — zero WordPress deps, unit-tested with php tests/test-routing.php.

The front-end depends on the adapter; the adapter depends on the core; the core depends on nothing. Dependencies only ever point downward.

FileLayerResponsibility
class-blr-routing-engine.phppureScore answers → tier → resolve outcome. The conditional logic.
class-blr-lead-validator.phppureSanitise + validate a submission (honeypot, consent, required fields).
class-blr-webhook-dispatcher.phppure*Build CRM payload, sign, retry, log. One optional wp_remote_post seam.
class-blr-ga4.phppureBuild + send the GA4 Measurement Protocol event.
class-blr-lead-repository.phpwpPersist a lead as the blr_lead CPT + meta.
class-blr-cpt / -fields / -rest / -shortcode / -elementor-widget / -adminwpRegister data structures, endpoints, placement, and the operator UI.
class-blr-core.phpwpBootstrap + wire everything on plugins_loaded.
03

The request lifecycle

Two REST calls, deliberately split. The first computes a routed view with no personal data. The second — only after the visitor commits — captures and fans out. Here's the full sequence.

From first click to CRM record
Browser
Renders the server-side wizard

The questions are already in the DOM (SEO + no-JS resilience). router.js hydrates: manages steps, focus, and pushes blr_* events to the GTM dataLayer.

Browser → REST
POST /braven/v1/route

Sends buyer type + track + qualifier answers. No PII. The server runs the routing engine and returns the decision — tier, outcome, tailored copy, the form fields to collect.

Browser
Shows the routed view

Tailored headline, the buyer's value proposition, and only the right form fields. Fires blr_lead_routed. The visitor fills in contact details.

Browser → REST
POST /braven/v1/lead

Contact fields + selection + GA client id + UTMs + the nonce. The server recomputes the decision (never trusts the client) and validates.

WordPress
Persist → CRM → Email → GA4

One handler orchestrates all four, in order, each degrading safely. Returns the lead id + destination.

Browser
Success state

Fires blr_lead_submitted and shows the next step (booking link / download / confirmation).

Why split route from lead? The routed view can render — and the routed dataLayer event can fire — before asking for a single piece of contact data. That's lower friction, and it lets Braven A/B the routed copy without collecting PII to do it.
04

The routing engine, in full

This is the part the “Routing Test” asks about. It's deliberately transparent: every answer carries points, the sum picks a tier, and a short ordered rule list turns (buyer type, tier, funding posture) into an outcome. All of it is config in data/routes.php — the engine just reads it.

Step 1 — Score the answers

QualifierAnswer → points
FundingFunded 40 · Budgeted 25 · Seeking 10 · Exploring 0
TimelineThis quarter 30 · 6 months 20 · This year 10 · Unsure 0
RoleApproves budget 20 · Recommends 12 · Researching 4
Scale500+ 20 · 100–500 14 · 25–100 8 · <25 3
Segment weightCity / County / Foundation +10 · Chamber / Corporate +5

Step 2 — Map the score to a tier

Tier A · High intent
score ≥ 70
Tier B · Qualified
35 – 69
Tier C · Nurture
< 35

Step 3 — Resolve the outcome (first match wins)

#ConditionOutcomeNext step
1Foundation + funding is seeking/exploringFunding partnershipFunding intake + playbook
2Tier ABook a callBooking link + short confirm form
3Tier BRequest a proposalFuller intake (adds goals)
4Tier C (default)NurtureProgram overview + newsletter

Worked examples

A funded city, ready this quarter, decision-maker, 500+ businesses

funding funded 40 + timeline quarter 30 + role decision 20 + scale xl 20 + city weight 10 = 120

120 ≥ 70 → Tier A. No foundation rule applies → outcome Book a call, with the city value prop: “Turn ARPA / CDBG / general-fund dollars into measurable small-business growth your council can point to.”

A budgeted county, this-year timeline, recommender, under-25 businesses

funding budgeted 25 + timeline year 10 + role recommend 12 + scale small 3 + county weight 10 = 60

35 ≤ 60 < 70 → Tier B → outcome Request a proposal (the fuller intake that also collects goals).

A foundation seeking a grant — even with otherwise-hot answers

The score might land in Tier A, but rule #1 matches first: a foundation whose funding is seeking or exploring is routed to Funding partnership. A specific rule intentionally beats the generic tier — because for a funder, the right next step is structuring a fundable program, not a sales call.

The code

// class-blr-routing-engine.php — the whole decision, no side effects
public function decide( array $selection ) {
  $score    = $this->score( $answers, $buyer_type );   // sum of points + segment weight
  $tier_key = $this->tier( $score );                  // A / B / C by threshold
  $outcome  = $this->resolve_outcome( $buyer_type, $track, $tier_key, $answers ); // first match
  return [
    'tier' => $tier_key, 'outcome' => $outcome_key,
    'headline' => $outcome['headline'], 'form_fields' => $outcome['form_fields'],
    'value_prop' => $bt['value_prop'], 'track_label' => $tk['label'], /* … tailored copy … */
  ];
}
Extending it is a config edit. A new buyer type, a re-weighted qualifier, a re-pointed destination — all live in data/routes.php. The engine never changes; the tests catch a mistake in a second.
05

The data model

Right tool per job. Leads and videos are Custom Post Types (they want an admin UI, search, REST, and ACF). The webhook delivery log is high-volume append-only audit data, so it's a real custom table. Videos get two taxonomies so terms stay indexed and filterable.

Entities & relationships
blr_lead CPT
  • ID bigint
  • post_title org — type/track
  • _blr_email meta
  • _blr_organization meta
  • _blr_buyer_type meta
  • _blr_track meta
  • _blr_intent_tier A/B/C
  • _blr_intent_score int
  • _blr_outcome meta
  • _blr_crm_status meta
  • _blr_crm_key idem key
1 — Nby lead_id
{prefix}blr_deliveries table
  • id bigint AI
  • lead_id FK · KEY
  • idem_key varchar · KEY
  • attempt smallint
  • http_status smallint
  • result delivered/retry/failed
  • message varchar
  • created_at datetime
blr_video CPT
  • ID bigint
  • post_title / excerpt core
  • _blr_video_url meta
  • _blr_video_duration meta
  • menu_order ordering
N — N
blr_track taxonomy
  • Digital Marketing
  • Social Media
  • AI
N — N
blr_proficiency taxonomy
  • Beginner
  • Intermediate
  • Advanced

Why a CPT for leads but a table for deliveries?

A CPT gives leads the entire WordPress ecosystem for free — the admin list, search, capabilities, export, and (critically for Braven's stack) ACF Pro field groups mapping straight onto the same _blr_* meta keys. Delivery attempts are different: many rows per lead, write-heavy, never edited. That's a table with indexes on lead_id and idem_key, created via dbDelta() on activation.

// class-blr-cpt.php
register_post_type( 'blr_lead', [ 'public'=>false, 'show_ui'=>true, 'show_in_rest'=>false ] ); // private: no public URL, no public REST
register_taxonomy( 'blr_track', 'blr_video', [ 'hierarchical'=>true, 'show_admin_column'=>true ] );
06

The capture pipeline

One POST to /braven/v1/lead runs four stages in order. Each degrades safely — a failed email never fails capture, an unconfigured CRM is recorded rather than thrown. The lead is captured no matter what downstream does.

Inside the /lead handler
Validatesanitise · honeypot · consent · recompute decision server-side
Persistblr_lead CPT + meta · dedupe by idem key
CRMwebhook + retries + delivery log
Emailteam alert + prospect reply + hook
GA4server-side generate_lead
// class-blr-rest.php — the orchestration, trimmed
public static function post_lead( $req ) {
  if ( ! self::verify_nonce( $req ) ) return 403;          // CSRF
  $decision = ( new BLR_Routing_Engine )->decide( $selection ); // recompute — never trust client
  $result   = ( new BLR_Lead_Validator )->validate( $params, $decision['form_fields'] );
  if ( $result['errors']['_spam'] ) return 200;             // silent to bots
  if ( ! $result['ok'] ) return 422;                       // field errors

  $lead_id = ( new BLR_Lead_Repository )->save( $lead, $decision );          // 1. persist
  $crm     = ( new BLR_Webhook_Dispatcher )->dispatch( $lead, $decision, $lead_id ); // 2. CRM
  ( new BLR_Email_Workflow )->run( $lead, $decision, $lead_id );               // 3. email
  ( new BLR_GA4 )->generate_lead( $decision, $ga_client_id );                  // 4. GA4
  return [ 'ok'=>true, 'lead_id'=>$lead_id, 'crm'=>$crm, 'destination'=>$dest ];
}
Idempotency runs through the whole pipeline. A stable 32-char key (email + org + type + track + the hour) means a double-fired form updates the same blr_lead instead of twinning it, and a webhook retry collapses to one CRM record.
07

The CRM handoff

The lead POSTs to a configurable webhook — Braven's Smrts CRM inbound hook, or a Zapier/Make catch-hook. The payload is CRM-neutral and signed; delivery is idempotent and retried; every attempt is logged and shown in the admin Routing Console.

The payload

{
  "idempotency_key": "a1b2…",               // dedupe/upsert key (also an X-Braven-Idempotency header)
  "lead_id": 30,
  "contact": { "name":"…", "title":"…", "email":"…", "phone":"…", "organization":"…" },
  "segmentation": {
    "buyer_type":"city", "track":"ai",
    "intent_tier":"A", "intent_score":120,
    "outcome":"book_call", "priority":"hot"
  },
  "notes": "…goals…", "consent": true,
  "attribution": { "utm_source":"…", "gclid":"…" },
  "source": "braven-lead-router", "page_url":"…", "submitted_at":"2026-…Z"
}

Mapping to Smrts (or any CRM)

Payload pathMaps to
contact.*Contact / Lead record fields
segmentation.buyer_type / trackLead source segment / interest tag
segmentation.intent_tier + intent_scoreLead score / priority
segmentation.outcomePipeline stage / workflow trigger
attributionSource attribution fields
idempotency_keyDe-dupe key (upsert)

Reliability

Idempotent

Retries and double-submits collapse to one CRM record via the key.

Signed

Optional HMAC-SHA256 over the body as X-Braven-Signature so the CRM can verify authenticity.

Retried

Timeouts, 5xx, 429 retry with short backoff; 4xx are terminal. Every attempt is logged.

In the demo the webhook points at a local mock sink, so the Routing Console shows real delivered rows end-to-end. In production you paste the Smrts inbound URL into Settings and delete the mock — no code change.
08

Tracking & GA4

Two layers. The browser pushes a clean event to dataLayer at every step for GTM. The server independently fires the conversion (generate_lead) via the GA4 Measurement Protocol — so a lead is recorded even when an ad-blocker or a bounce kills the client beacon. That redundancy is the “server-side tagging a plus.”

dataLayer eventFires whenKey params
blr_router_startTool loads
blr_buyer_type_selectedStep 1 chosenbuyer_type
blr_track_selectedStep 2 chosentrack
blr_qualifiers_answeredStep 3 completeanswers
blr_lead_routedRouted view shownbuyer_type, track, intent_tier, outcome
blr_lead_submittedCapture succeedsintent_tier, outcome, lead_id
Client + server, stitched to one GA4 session
router.jsdataLayer.push per step
GTM (GTM-535B5NR)Custom Event triggers → GA4 tags
GA4funnel events
Servergenerate_lead via Measurement Protocol · same client_id from _ga cookie

Wiring it in GTM

For each blr_* event: a GA4 Event tag on a matching Custom Event trigger; map buyer_type, track, intent_tier, outcome to registered GA4 custom dimensions. The GTM container prints into wp_head only when a container ID is set — nothing loads otherwise.

09

The wizard front-end

Progressive enhancement over server-rendered markup: the questions exist in the DOM before JavaScript runs. router.js is a tiny (~7KB, no jQuery) state machine that handles transitions, focus, REST calls, and the dataLayer.

The client state machine
Step 1buyer type
Step 2track
Step 3qualifiers
Step 4routed view + form
Successnext step
  • Auto-advance on the single-choice steps (buyer type, track) keeps it snappy; qualifiers wait for an explicit “See my path”.
  • Focus moves to each new step and errors are announced via an aria-live region.
  • The routed form is built from the decision — the server tells the client which fields to collect, so the form matches the outcome (a booking confirm asks for less than a proposal intake).
  • GA client id + UTMs are read client-side and sent with the capture so server-side GA4 stitches to the same session and the CRM gets attribution.
10

How it lives inside WordPress

Everything is stock WordPress — no core hacks, no exotic dependencies. One plugin folder in wp-content/plugins/ plus a hello-elementor child theme. Drop it into any WordPress 6.4+ site and activate.

Custom featureWordPress primitive it uses
Captured leadsCPT blr_lead + post meta
Training videosCPT blr_video + taxonomies
CRM delivery logCustom table via dbDelta()
Editable fieldsACF Pro field group (native meta-box fallback)
Wizard & capture APIregister_rest_route()
PlacementShortcode and Elementor widget (one renderer)
Operator UIAdmin submenu + custom list columns
Trackingwp_head dataLayer + wp_remote_post to GA4
ExtensibilityAction hook blr_lead_captured

ACF Pro — a first-class citizen, never a hard dependency

Without ACF, meta is registered/typed and the admin shows a native panel. With ACF Pro (Braven's stack), the shipped field groups in acf-json/ take over the UI, bound to the same _blr_* keys — zero migration. ACF simply upgrades the interface.

Shortcode and Elementor — one renderer

Deep logic in PHP; Elementor stays clean. The “Braven Lead Router” widget carries no logic — it calls the shortcode renderer, so there's exactly one implementation. (Load-order note: the widget subclasses extend \Elementor\Widget_Base, which isn't available at plugins_loaded, so they're declared lazily inside the elementor/widgets/register callback.)

11

REST API reference

EndpointMethodBodyReturnsAuth
/wp-json/braven/v1/configGETbuyer types, tracks, qualifierspublic
/wp-json/braven/v1/routePOST{buyer_type, track, answers}{decision:{tier,outcome,headline,form_fields,…}}public (no PII)
/wp-json/braven/v1/leadPOSTselection + contact + ga_client_id + utm{ok, lead_id, crm, destination}REST nonce + honeypot
# Try the routing brain live — no auth, no PII:
curl -s -X POST https://braven-demo.levelbrook.com/wp-json/braven/v1/route \
  -H 'Content-Type: application/json' \
  -d '{"buyer_type":"foundation","track":"ai","answers":{"funding":"seeking","timeline":"year","role":"recommend","scale":"m"}}'
# → outcome: funding_partnership  (the foundation rule beats the tier)
12

Performance — how PageSpeed stays > 90

By removing weight, never by adding plugins. The whole thing is built to load almost nothing.

Conditional assets

CSS/JS enqueue only on pages that contain the tool. The rest of the site loads none of it.

No framework

Vanilla JS + PHP. No jQuery, no React, no build step. ~6KB CSS + ~7KB JS.

Config is OPcache'd

The routing matrix is plain PHP — no DB round-trip on the hot path.

Server-rendered

First paint doesn't wait on JavaScript; the wizard and library render on the server.

Library filters in-browser

One cached WP_Query; filtering is show/hide over the DOM — no AJAX per keystroke.

Elementor stays lean

It only loads its runtime on pages actually built with it; the tool pages are shortcode-rendered.

The Speed Test, answered: a multi-tiered searchable video library is a CPT + two taxonomies, rendered once server-side and filtered client-side. Indexed terms, cached query, static-friendly markup — never “install more plugins.”
13

Accessibility (WCAG 2.1 AA)

Braven's audiences are government and institutional, so accessible builds are non-negotiable. Built in from the start, not bolted on.

  • Each question is a <fieldset> with a <legend>; options are real <input type="radio"> inside a role="radiogroup" — keyboard operable, screen-reader correct.
  • Visible focus states on every control (never outline:none without a replacement).
  • Step changes move focus to the new step and are announced via aria-live; errors are announced too.
  • Colour contrast meets AA; state is never colour-only.
  • Server-rendered questions work without JavaScript; a <noscript> fallback offers a phone number.
  • prefers-reduced-motion disables the shake/entrance animations.
14

Security

CSRF

The capture endpoint requires a valid WordPress REST nonce.

Spam

A hidden honeypot; bots that fill it get a silent 200 and nothing is stored.

Input

Every field sanitised at one choke point; email validated; output escaped.

Privacy

Leads are a private CPT with show_in_rest=false; PII never on public REST. Meta auth requires edit_posts.

Server-authoritative

The decision is recomputed on capture — the client can't dictate the outcome.

Webhook integrity

Optional HMAC-SHA256 signature so the CRM verifies each payload.

15

The deploy stack

The live demo is a single self-contained container: FrankenPHP (PHP 8.4) serving a real WordPress + Elementor install on SQLite — no MySQL — behind an auto-TLS reverse proxy.

Runtime
BrowserHTTPS
kamal-proxyTLS · routes by host
FrankenPHPCaddy + PHP 8.4
WordPress + Elementorthe plugin + child theme
SQLiteon a persistent volume
  • First boot self-provisions headlessly via WP-CLI: installs core, the SQLite integration, Elementor, the plugin, and the child theme, then seeds pages, the menu, the video library, and settings.
  • The SQLite database lives on a mounted volume, so it survives image rebuilds.
  • Assets aside, the whole demo is reproducible from the repo: docker builddocker run → register the proxy route.
16

Testing

The routing brain has no WordPress dependency, so the entire contract is unit-tested with plain php tests/test-routing.php — 26 assertions, no database, no HTTP server.

CoversExamples
Scoringhot answers + city weight = 120; cold + chamber = 12
Tier boundaries70→A, 35→B, 34→C
Decisionscity/AI hot → book_call; county mid → request_proposal; chamber cold → nurture
Rule precedencefoundation + seeking → funding_partnership (beats Tier A)
Robustnessgarbage input never throws → defaults to nurture
Validationbad email rejected; missing consent rejected; honeypot caught; script tags stripped
CRM payload32-char idempotency key; stable for identical input within the hour
The tests found a real bug during the build: a fixture assumed a “mid-intent” county scored into Tier B, but the county segment weight pushed it to 75 (Tier A). The engine was right; the assumption was wrong — exactly what a test suite is for.
17

Extending it — the Phase 2 AI seam

Braven's spec: their AI team owns the generative logic; the engineer architects the hooks so it plugs in without a rebuild. The seams are already here.

Action hook

do_action('blr_lead_captured', $lead, $decision, $lead_id) fires on every qualified lead. An AI feature (e.g. a custom syllabus) subscribes with zero plugin changes.

REST namespace

braven/v1 is the natural home for a gated /syllabus or /worksheet endpoint reading the same lead data.

Typed data

Buyer type, track, tier, and answers are structured meta — clean inputs for a personalization layer to condition on.

18

Glossary

TermMeans
CPTCustom Post Type — a WordPress content type beyond posts/pages (here: leads, videos).
ACF (Pro)Advanced Custom Fields — the field-UI plugin Braven uses; the plugin ships field-group exports.
TaxonomyA WordPress classification system (here: track, proficiency for videos).
dataLayerThe array GTM reads; the wizard pushes funnel events onto it.
Measurement ProtocolGA4's server-to-server event API — used for the redundant conversion.
Idempotency keyA stable hash so retries/double-submits don't create duplicates.
Intent tierA / B / C bucket derived from the qualifier score.
FrankenPHPA modern PHP app server (Caddy + PHP) running the demo container.

← Build docs · Live tool · Video library

A working reference for Braven Agency · Source and tests included.