Mox Technologies

Case study · demo build

Lead router: form to CRM to text to task, with GoHighLevel and n8n

Every form lead lands in the CRM once, gets a personal text, and creates a follow-up task with the right due time. The demo page shows each step as it happens.

This is a portfolio build, not a client project. The workflow runs on a real, self-hosted n8n server. The GoHighLevel calls go to a sandbox I built against the published API v2 contract (same paths, headers and required fields), and no real text messages are sent. Pointing it at a real GoHighLevel sub-account means changing three config values.

1.8 to 3.9 sform to finished, 4 timed runs
47automated tests
19n8n nodes, every CRM step error-handled

The problem

Teams buy a CRM and then still copy leads in by hand, or connect the form with a single zap that breaks quietly the first time an API hiccups. Two things matter most. The lead should never be lost, and the same person shouldn't end up in the CRM three times.

What I built

An n8n workflow with a webhook trigger. It validates the lead, normalizes the phone number to E.164, and upserts the contact into GoHighLevel. Then it sends a text written for buyers or sellers and creates a task. Leads planning to move within three months get a task due in 15 minutes. Everyone else gets one due in 24 hours. Each step reports to a timeline, and the demo page polls that timeline so you can watch the run.

The public page never talks to n8n directly. Form posts go through a small serverless function (validation, a rate limit of five per minute per visitor, and a link filter for spam), and from there over a private tunnel to the n8n server. n8n itself isn't reachable from the internet.

How it works

Form ──▶ /api/routing/submit  (validate, rate limit)
            │  private tunnel, no public n8n URL
            ▼
         n8n: Validate ▶ Upsert contact ▶ Send SMS ▶ Create task ▶ Respond
                              │ any failure ──▶ failed event + 502 with the step name
                              ▼
         timeline events ──▶ live demo page

Decisions that matter

CRM failures and logging failures are handled differently. If GoHighLevel rejects a step, the run takes an error branch, records which step failed, and returns a 502 so the form can say so. If the timeline logger fails, the run continues, because a logging problem should never cost you a lead. Contacts are deduplicated by email first and then by phone within the location, the same priority GoHighLevel documents, so resubmitting updates one contact.

Configuration lives in one node at the top of the workflow. Newer n8n blocks environment variables inside nodes by default, so this keeps the workflow portable between servers.

What the tests caught

The first activation on the server failed. The workflow used a version of the If node that this n8n release doesn't ship. There's now a test that checks every node's version against the target release.

The second problem was more interesting. All the unit tests passed, yet the live run rejected every lead. In real n8n, the payload sits under the webhook node's body, and the validation step was reading the config node's output instead. The test harness had made the same wrong assumption as the code, so it couldn't catch it. I rewired the harness to mirror real n8n, confirmed it fails against the old code, then fixed the code.

The last one only showed up in a real browser. The run finished on the server, but the timeline stayed empty because the list and the Timeline dropdown shared an element id, so the steps were rendered into the dropdown. A test now fails on any duplicate id in the page.

Taking it to production

The token moves into n8n's credential store. CRM calls get retries with backoff and an idempotency key. Runs that still fail land in a dead-letter queue with the full payload so they can be replayed. Before sending real texts, the form captures SMS consent with a timestamp.

Stack