Their ops manager was copying data by hand. A contractor signed their Deel agreement at 9 AM. By 11 AM, nobody had created the ClickUp task. By 3 PM, someone remembered to update the HubSpot deal stage. By end of day, the client had already emailed asking where their project kickoff was. This was not a people problem. It was an infrastructure problem — and it was destroying trust at the exact moment new contractors were supposed to feel confident joining the team.
The company came to me running three disconnected platforms: GoHighLevel as a holdover CRM from a previous agency, Deel for contractor contracts and payments, and ClickUp for project delivery. None of them talked to each other. The result was a manual coordination tax paid by every person on the team, every single day.
Here is exactly what I built to fix it — and why the architecture matters as much as the tooling.
Before touching a single integration, I ran a full GTM stack audit. What I found was a GoHighLevel account originally set up for a lead-gen campaign — now being used as a makeshift client CRM. Custom fields were misconfigured. Pipeline stages had drifted from reality. Contact records were duplicated across campaigns. Automation sequences were firing on the wrong triggers.
GoHighLevel is a strong platform for what it's designed to do — high-volume local service marketing, SMS-heavy follow-up, and automated appointment booking for B2C businesses. It is not designed to be a B2B relationship CRM for a services company managing ongoing contractor relationships, project deliverables, and complex deal structures. Using it in that context was not a GHL problem. It was a wrong-tool problem.
The migration was not a copy-paste operation. Pulling raw GHL data and dumping it into HubSpot would have simply transferred the chaos. The approach was to rebuild the data model correctly in HubSpot first — then migrate only clean, structured records.
HubSpot's native object structure — Contacts, Companies, Deals, Tickets — needed custom properties defined before a single record touched the system. For this client, the critical custom properties were:
pending_signature, active, paused, terminatedThese properties were not decorative. Every automation, webhook handler, and workflow downstream depended on these fields being populated accurately. Schema-first migration is the only migration worth doing.
GHL DATA EXPORT LAYER ───────────────────────────────────────────────────────────── GHL Contacts ──► CSV Export (Contacts + Custom Fields) GHL Companies ──► CSV Export (Accounts) GHL Deals ──► CSV Export (mapped to HubSpot Pipeline Stages) GHL Notes ──► Separate export; re-attached post-import via API TRANSFORMATION LAYER (Google Sheets + Custom Script) ───────────────────────────────────────────────────────────── ► Deduplicate by email (keep most recent record) ► Normalize phone formats (+[country_code][number]) ► Map GHL pipeline stages → HubSpot lifecycle stages ► Flag records with missing Deel Contract ID for manual review ► Strip GHL automation tags irrelevant to HubSpot workflows HUBSPOT IMPORT LAYER ───────────────────────────────────────────────────────────── ► Import Companies first (parent records) ► Import Contacts with Company association ► Import Deals with Contact + Company associations ► Re-attach Notes via HubSpot API (batch endpoint) ► Run duplicate merge via HubSpot Duplicate Management tool
The GHL export produced 847 contact records. After deduplication and cleanup, 612 unique, valid records were imported into HubSpot. The 235 records removed fell into three categories: duplicate email entries (147), test/spam form submissions (61), and cold leads from old campaigns with zero relevance to the current business model (27).
Each imported record was validated against a required-fields checklist before being associated to a deal. Any contact missing a company association, lifecycle stage assignment, or primary email was quarantined in a HubSpot list for manual review — not imported into the active pipeline.
"Clean data is not a nice-to-have. It's the load-bearing wall of every automation you're about to build on top of it." Arsalan Faysal, Revenue Systems Architect
Deel does not have a native HubSpot integration. The connection required a webhook-driven architecture via Make (formerly Integromat), with a custom middleware layer to handle event parsing, data transformation, and conditional routing logic.
Deel's webhook system fires on all meaningful contract lifecycle events. For this integration, four events drove the automation logic:
| Deel Webhook Event | Trigger Condition | Downstream Action |
|---|---|---|
contract.created |
New contractor contract generated in Deel | Create Contact in HubSpot; set Contractor Status → pending_signature |
contract.signed |
Contractor completes e-signature | Update HubSpot Contact Status → active; advance Deal Stage; create ClickUp onboarding task |
payment.completed |
Contractor payment processed in Deel | Log payment activity on HubSpot Contact timeline; update Payment Cycle date property |
contract.terminated |
Contract ended (any reason) | Update HubSpot Status → terminated; close associated Deal; notify account owner via HubSpot workflow email |
The Make scenario handling contract.signed was the most critical path — it triggered the largest downstream cascade. Here is how it was structured:
DEEL WEBHOOK: contract.signed
│
▼
[Make: Webhook Receiver Module]
► Parse JSON payload
► Extract: contract_id, contractor_name, contractor_email,
contract_type, start_date, pay_rate, currency
│
▼
[Router: Check if Contact Exists in HubSpot]
► Search HubSpot Contacts by email
├── EXISTS ──► Update Contact Properties
└── NEW ──► Create Contact + Associate to Company
│
▼
[HubSpot: Update Deal Stage]
► Find Deal associated to Contact
► Move Stage: "Contract Sent" → "Active / Onboarding"
► Set Close Date: start_date + 14 days (onboarding window)
│
▼
[ClickUp: Create Onboarding Task]
► Task Name: "Onboard [contractor_name]"
► List: Contractor Onboarding (configured per team)
► Assignee: Ops Manager (static)
► Due Date: start_date + 2 business days
► Custom Fields: Deel Contract ID, Pay Rate, Currency
│
▼
[HubSpot: Write Back ClickUp Task ID]
► Update Contact property: ClickUp Task ID = [task.id]
► Log timeline activity: "Onboarding task created in ClickUp"
│
▼
[HubSpot: Enroll Contact in Onboarding Workflow]
► Trigger internal notification to account owner
► Schedule Day 1 / Day 3 / Day 7 automated check-in emails
Deel webhooks do not guarantee delivery order. In high-volume environments, a contract.signed event can occasionally arrive before contract.created has been processed by HubSpot. The Make scenario includes a retry buffer with a 90-second delay loop — if the HubSpot Contact lookup returns null, the scenario waits, re-attempts the search twice, then routes to a fallback branch that creates the Contact from the signed event payload directly. Without this buffer, approximately 6–8% of rapid-fire contract events would fail silently.
The Deel → HubSpot → ClickUp path handled new contractor onboarding. But the team also needed HubSpot deal events — not just Deel contract events — to generate ClickUp tasks. A sales rep closing a deal in HubSpot should not have to manually create a project task in ClickUp. That is a system design failure, not a workflow issue.
Three HubSpot → ClickUp automation flows were built:
| HubSpot Trigger | ClickUp Output | Fields Passed |
|---|---|---|
| Deal Stage → Closed Won | Create Project Task in "Active Clients" list | Deal Name, Contact Name, Close Date, Deal Value, Owner |
| Deal Stage → Active / Onboarding | Create Onboarding Checklist Task | Contractor Name, Deel Contract ID, Start Date, Pay Rate |
Contact Property → contractor_status = terminated |
Update ClickUp Task Status → "Offboarded" | ClickUp Task ID (stored on HubSpot Contact record) |
The HubSpot → Make connection used HubSpot's native webhook action inside enrolled workflows — not polling. Every state change fires a webhook payload in real-time, which Make processes within seconds. No scheduled batch syncs. No data lag. The operational record in ClickUp is always current.
Bidirectional sync was important. When the ops team updated a task status in ClickUp — marking onboarding as complete, flagging a blocker, or closing a project — that status needed to surface in HubSpot without anyone touching the CRM manually.
ClickUp's webhook system fires on task status changes. A separate Make scenario listened for task.statusUpdated events from the designated onboarding lists and wrote the updated status back to the corresponding HubSpot Contact or Deal record using the stored ClickUp Task ID as the matching key.
CLICKUP WEBHOOK: task.statusUpdated
│
▼
[Make: Extract task_id + new_status]
│
▼
[HubSpot: Search Contact where ClickUp_Task_ID = task_id]
│
├── FOUND ──► Update Onboarding Stage property
│ Log timeline note: "ClickUp status → [new_status]"
│
└── NOT FOUND ──► Route to Deal search
Update Deal custom property: Project Status
The cleanest demonstration of the architecture is the end-to-end contractor activation sequence. A single event — a contractor signing their Deel agreement — now cascades through all three platforms without a single human in the loop.
EVENT: Contractor signs Deel agreement
──────────────────────────────────────────────────────────────────
DEEL
└── Fires webhook: contract.signed (payload: contractor email,
contract_id, start_date, pay_rate, currency, contract_type)
│
▼
MAKE (Middleware)
└── Receives payload
└── Searches HubSpot for Contact by email
└── Updates Contact: contractor_status → "active"
└── Updates Deal Stage: "Contract Sent" → "Active / Onboarding"
└── Creates ClickUp task: "Onboard [Contractor Name]"
└── Writes ClickUp task_id back to HubSpot Contact
└── Enrolls Contact in HubSpot onboarding email workflow
│
─────┴─────
│ │
▼ ▼
HUBSPOT CLICKUP
Contact updated Task created:
Deal stage advanced "Onboard [Name]"
Timeline logged Due: start_date + 2 days
Workflow enrolled Custom fields populated
Owner notified Assigned to Ops Manager
──────────────────────────────────────────────────────────────────
TOTAL ELAPSED TIME FROM CONTRACT SIGNATURE TO FULL SYSTEM UPDATE:
~8–12 seconds
MANUAL STEPS REQUIRED: 0
"An eight-second automated cascade replaced four hours of manual coordination. That's not a productivity improvement. That's a different category of operations." Arsalan Faysal, Revenue Systems Architect
The technology was the execution layer. The real outcome was operational visibility the business had never had before.
Before this system, if you asked the founder "how many active contractors do we have right now, and what's their onboarding status?" — the answer involved opening three tabs, cross-referencing a spreadsheet, and waiting for someone on Slack to confirm. After the integration, the answer lived in a single HubSpot dashboard, updated in real-time, with a full audit trail of every status change.
| Capability | Before | After |
|---|---|---|
| Contractor onboarding task creation | Manual; avg. 4.5-hour lag | Automated; <12 seconds from contract signature |
| HubSpot deal stage accuracy | Updated manually; often days late | Event-driven; real-time |
| CRM data integrity | 847 records; 28% duplicates or invalid | 612 clean, validated records; 100% integrity |
| Payment visibility in CRM | Zero; Deel was a separate silo | Payment events logged on HubSpot Contact timeline |
| Offboarding notification | Verbal or Slack; no system record | Automated; triggers HubSpot workflow + ClickUp status update |
| Cross-platform record linking | None | Every Contact in HubSpot has Deel Contract ID + ClickUp Task ID stored |
The specific platforms here — Deel, HubSpot, ClickUp, GHL — are not the point. The architecture pattern applies to any multi-platform operational stack where HR/contracting data, CRM data, and project management data live in separate silos with no programmatic connection between them.
The diagnostic questions are always the same:
If you answered "a human" to question two or "more than one" to question four — you have a systems problem, not a staffing problem. Adding headcount to manually coordinate disconnected platforms is a compounding liability. Every new hire amplifies the cost of the infrastructure gap, not the output.
The engagement model is direct. You work with me, not a team of junior automation specialists. The system is built, tested, documented, and handed over with full operational training. You own the infrastructure permanently — it doesn't break when an agency relationship ends.