Last updated 26 February 2026

CRM Stack Migration

Building Attio for Gen H. Replacing HubSpot with a CRM that models how the business actually works, then layering in automation and AI to make BDMs measurably more effective.

Actionable Sara validated Building demos

Validated by Sara (26 Feb)

Why We're Moving

HubSpot costs ~$5k/month and fails at all three things it does: CRM doesn't model the business (no broker/brokerage/network hierarchy), email marketing is weak because the data feeding it is weak, and live chat is fine but overpriced. The core problem: bad tooling is actively making the sales team worse. They can't find the right prospects, can't see the right context, can't act on the right signals.

Contract expires December 2026. Comfortable timeline.

Target Stack

FunctionCurrentTargetStatus
CRMHubSpotAttio (Pro, £68/user/month)Eval complete, building demos
Email marketingHubSpotCustomer.ioNot started, depends on CRM decision
Live chatHubSpotHelp Scout or IntercomNot started, lowest priority

What We've Built So Far

Data model (6 custom objects, 49+ attributes)

ObjectKey attributesRelationships
BrokersName, phone, FCA number, activity status, cases 90d, total lending 90d, BDM owner→ Brokerage
BrokeragesName, FCA number, firm type, active brokers, total cases 90d, BDM owner→ Network, → Club
NetworksName→ Parent Network
ClubsName(standalone)
CasesReference, applicant name, property address, loan amount, LTV, stage, submitted date→ Broker, → Brokerage
LoansAccount number, borrower name, loan amount, interest rate, maturity date→ Broker, → Brokerage, → Originating Case

Seed data

Display configuration

Each object's Record Text (Workspace settings > Objects > Appearance) must be set to the human-readable field. Without this, the UI shows UUIDs everywhere.

ObjectRecord Text field
BrokersName
BrokeragesName
NetworksName
ClubsName
CasesReference
LoansAccount Number

Attio Pro Platform Capabilities

Workflows (automation engine)

Triggers: Record created, record/attribute updated, recurring schedule (daily/weekly/cron), webhook received, manual button, task created, record added to list.

Actions: Create/update records, create tasks, complete tasks, enrol in sequences, if/else branching, switch (multi-path), filter, delay, formula calculations, aggregate values (sum/avg/min/max), Slack messages, in-app broadcast alerts, HTTP requests, AI actions (classify, summarise, prompt completion), loop, parse JSON.

Limits: 10,000 credits/month on Pro. Find Records capped at 100 results per query. HTTP requests timeout after 20 seconds.

Sequences (email drips)

Multi-step email campaigns. Smart sending (timezone-aware, business hours only). Auto-pause on out-of-office. Auto-exit on reply. Threading (conversation replies). Can be triggered manually or automatically via workflow "Enrol in Sequence" action.

Calculated fields

No native formula or rollup field types. Workaround: workflow-based calculations using Formula, Aggregate Values, and Adjust Time blocks that write results to regular fields. Values update when the workflow runs, not in real time. Good enough for daily metrics.

Kanban

Custom fields on cards (any attribute as a card row). Native "Track time in stage" - toggle per stage, set target duration, counter appears on cards and goes red when exceeded. Sorting by time in stage. Stage-level aggregations (sum, count).

Notifications

In-app notifications for @mentions, task assignments, daily task digest. Condition-based alerts require workflows: Broadcast Message (in-app popup) or Slack integration. No native "notify me when X condition is met" without a workflow.

Open Design Question: Interaction Type Modelling

Sara wants structured tracking of how the team interacts with brokers - not just "a note was added" but "this was a sales meeting" vs "this was a half-day hot-desking session" vs "this was a phone call." With drop-downs, not free text.

The problem: Attio's native activity system (timeline entries, logged meetings, calls) has a fixed schema with no custom attributes. You cannot add an "interaction type" select field to a native activity entry.

Option A: Custom Interactions object only

Fully structured and queryable, but records don't appear in the broker's native activity timeline. BDM has to look in two places.

Option B: Note templates only

Appears in timeline but nothing is queryable. Can't answer "how many meetings this month?" Free text, consistency depends on discipline.

Option C: Hybrid - native activities + summary attributes

Aggregate reporting works ("which brokers haven't been visited in 3 months?") but no per-interaction detail.

Option D: Custom Interactions object + auto-mirrored note (recommended)

Create the custom Interactions object for structured data (type, date, duration, location, broker, outcome). When an Interaction record is created, a workflow fires that also creates a note on the linked broker record with the key details. The note appears in the broker's activity timeline for day-to-day use. The Interaction record lives in its own queryable views for management reporting.

The BDM never does anything twice. One input, two outputs. The Air Call integration makes this even more compelling - phone calls flow in automatically with AI summaries, creating both the Interaction record and the timeline note without any manual effort.

A bit of data duplication (structured record + summarised note), but workflows automate it and the UX wins for both BDMs and managers.

ApproachStructured per interactionIn timelineQueryableEffort
A: Custom object onlyYesNoYesMedium
B: Note templates onlyNoYesNoLow
C: Hybrid (aggregates only)PartialYesPartialLow-medium
D: Custom object + mirrored noteYesYesYesMedium

Air Call Integration

Air Call is Gen H's exclusive telephony platform. Every broker call goes through it. This is core infrastructure, not a nice-to-have.

Gen H's Air Call plan

Custom Professional, £3,585/month. 31 users, 15 numbers, 9 teams. AI Assist included in plan (call summaries, key topics, sentiment, action items) - was not enabled for any users until 26 Feb. Now enabled for one inbound BDM as a test; will roll out to full team if summary quality is good. No additional cost.

Why the native Attio integration won't work

A first-party Attio + Air Call integration exists (shipped early 2025). It auto-creates notes on Person records after calls. But it only logs to Person records, not custom objects. Our brokers are a custom object. It also doesn't pass through AI Assist summaries, recordings, or transcriptions, and can't trigger Attio workflows.

Recommended: Air Call webhook → Cloudflare Worker → Attio API

Air Call fires webhook events on call end. The payload includes: phone number, direction, duration, timestamp, agent, tags, notes, recording URL, and (with AI Assist enabled) an AI-generated call summary.

A lightweight Cloudflare Worker receives the webhook and:

  1. Matches the phone number against Attio's Brokers object
  2. Creates an Interaction record (type: Phone Call, duration, direction, AI summary, linked broker) - for structured querying
  3. Creates a note on the broker record (clean summary) - for the activity timeline
  4. Updates broker attributes (last interaction date, total calls, last call direction)
  5. If no matching broker: flags as potential new lead, creates a task for the BDM

This is better than the native integration because we control the schema and get AI summaries flowing through. It also establishes the template for other inbound data feeds (Ignite, email tracking, event attendance) - same pattern. Sub-second latency, near-zero hosting cost, ~100 lines of code. The BDM hangs up and the system does the rest.

The CRM Vision: What "Great" Looks Like for BDMs

The CRM should be proactive, not passive. A BDM opens Attio on Monday morning and their day is planned. They don't have to remember who to call, which cases are stuck, or which brokers are going cold. The system tells them.

Pipeline management (keeping what we have)

Auto-generated tasks at every stage transition and time threshold. The BDM's task list IS their daily plan.

TriggerAuto-task
Case enters DIP Submitted"Chase underwriting if no movement in 48hrs" (with delay)
Case exceeds target days in stage"Escalate [case ref] - [X] days in [stage]"
Case reaches Offer Issued"Call [broker] - offer issued. Discuss completion, ask about next case."
Case completes"Post-completion call to [broker]. Congratulate, ask for referrals."
High-value case created (>£500k)"Priority: [case ref] is £[amount]. Flag with underwriting."

Pipeline filling (getting more)

This is where CRM creates real edge. Four broker segments, each with different data requirements:

Gone-cold brokers (used to submit, stopped)

Data already exists. Daily workflow finds brokers where last submission > 30 days, flips status to "Cooling", creates re-engagement task. Auto-enrols in email sequence.

Buildable today

Low-volume brokers (active but under-indexing)

Compare individual broker volume against brokerage average. If a firm sends 15 cases/quarter across 3 brokers and one sends 1, that's visible. Also compare product mix - if broker sends 100% purchase but we're competitive on remortgage, flag the gap.

Partially buildable - full potential sizing needs market data

Cold prospects (zero interaction)

Brokers who've never submitted with Gen H. The CRM is the engine, not the fuel - data must come from outside. Candidate external feeds include: Ignite (daily broker search data - shows what criteria brokers searched for yesterday, includes contact details), FCA register, network membership lists, market data, event attendee lists. Pattern: ingest daily feed into Attio, generate targeted outbound workflows. Sara confirmed the Ignite feed works this way in practice.

Needs external data feed - candidates identified

Referral mining

Track "referred by" on broker records. When a broker reaches active status (3+ cases), auto-create task: "Ask [broker] for introductions." Real example already exists: Tom Richards referred Priya Sharma.

Buildable today

Automatic workflows

TriggerActionOutcome
Case stage changesReset days-in-stage counter, update last-stage-change dateClean pipeline tracking
Daily schedule (6am)Find stuck cases, create prioritised tasksMorning brief without AI
New case createdAuto-populate brokerage and network from broker recordData integrity
Broker's 3rd case createdFlip status from "New" to "Active", notify BDMTracks development
Case completesRecalculate broker totals (completions, lending, last date)Scorecard stays current
Broker added to Watch ListBroadcast alert to BDMRisk pushed to them
Off-panel broker submitsFlag on case + alert: "Off-panel at [brokerage]. Handle with care."Risks surfaced in context

Email sequences

Sara's taxonomy: welcome, nurture, nudge, win-back. Each sequence needs an ownership decision - who does it send from?

Ownership model (confirmed with Sara 26 Feb):

TriggerSequenceSends fromSteps
First case submittedWelcomeBDMDay 0: Thanks + what to expect. Day 3: Case update + our process. Day 7: How was the experience?
Case completesNurtureBDMDay 0: Congratulations. Day 14: Remortgage pipeline? Day 30: What's new at Gen H.
DIP submitted, no response 3dNudgeBDMDay 3: Need any help with this case? Day 7: Here's what we can do.
30 days inactiveWin-backMarketing (with BDM details)Day 0: Anything we can help with? Day 7: What's changed at Gen H. Day 14: Product areas we might be missing?
Added to conference listPre-event warm-upMarketingDay -7: Looking forward to it. Day -1: Find us at stand X. Day +2: Follow-up on what we discussed.

Claude Integration Roadmap

Attio proves its value standalone first. Claude is the accelerant, layered in after trust is established.

Phase 1 - Read-only
Morning brief skill
A /morning-brief Claude Code skill that queries Attio via API, synthesises the picture, and outputs 3-5 prioritised items with reasoning and suggested actions. Not a dashboard summary - an opinionated briefing.
Phase 2 - Write with confirmation
Post-meeting note processor
BDM finishes a call, dictates or types a quick summary. Claude extracts: what was discussed, what was promised, next action. Proposes CRM updates. BDM confirms, Claude executes via API. Critical: must show proposed actions before executing.
Phase 3 - Read
Natural language CRM queries
"Brief me on David Patel before I call him." "Which of my cases haven't moved stage this week?" "I'm meeting Openwork tomorrow - show me everything about Primis network brokers." Via MCP server (official OAuth-based or community API-key-based, both untested).
Phase 4 - Write with confirmation
Scratchpad bulk updater
BDM dumps a day's worth of unstructured notes. Claude parses, proposes a batch of CRM actions (notes, tasks, new records, deadline updates), BDM confirms all at once. Biggest time-saver but highest trust requirement.

Example morning brief:

1. GH-2026-016 is stuck. Rebecca Stone's case has been in Full App for 14 days. Lucy Harrison hasn't heard from us. Call her today with a concrete update or she writes us off.

2. David Patel is becoming your best broker. 4 cases in 3 weeks, 100% completion rate. Still off-panel at SPF. The panel review on 15 March is your window - bring his stats.

3. Emma Thompson has gone quiet. Her one case is at Valuation. If it's clean, this is the moment she decides if we're a one-off or a regular lender. Don't let the result land without a call.

Demo Build List

What to build now to demonstrate value to the sales team.

In Attio UI

  1. Turn on "Track time in stage" on Cases kanban - set targets: DIP Submitted 3d, Full App 7d, Valuation 5d, Offer Issued 10d. Cards go red when stuck.
  2. Workflow: case stuck auto-task - daily schedule, find cases exceeding stage target, create task for BDM.
  3. Workflow: new broker first case - on case created where broker case count = 1, create welcome call task.
  4. Workflow: gone cold detection - daily schedule, find brokers with last submission > 30 days, flip to "Cooling", create re-engagement task.
  5. Workflow: offer issued task - case stage changes to Offer Issued, create relationship call task.
  6. Workflow: case completed task - case stage changes to Completion, create post-completion call task.
  7. Email sequence: new broker welcome - 3 emails (day 0, 3, 7), wired to workflow #3.
  8. Build views - "My Tasks Today" (filtered, sorted by priority), "Pipeline Health" (cases by days in stage), "Broker Activity" (sorted by last submission), "Growth Opportunities" (low-volume brokers by brokerage).

Via API

  1. Add "Referred By" field to Brokers - record-reference to Brokers. Backfill Tom Richards → Priya Sharma.

Demo narrative: "You open Attio on Monday morning. Your task list tells you who to call and why. Cases that are stuck are flagged before the broker chases you. New brokers get a welcome sequence without you lifting a finger. Brokers going cold get caught at 30 days, not 90. Every offer and completion triggers a relationship touchpoint. And you can see your entire pipeline health in one view. HubSpot can't do this because it doesn't understand brokers, brokerages, and networks. Attio can because we built it to match how Gen H actually works."

Open Questions for Attio Team

Data model and configuration

Workflows and automation

Sequences

Integration and API

Migration

Timeline

This week
Build demo items 1-8 in Attio UI. Add referred-by field via API.
Early March
Demo to sales team. Go/no-go on committing to migration.
March
If yes: book Attio team call (open questions above). Begin data model refinement with real data. Test assert/upsert for data sync.
April
Customer.io evaluation. Map HubSpot data migration. Build morning brief skill.
May - June
Parallel run: Attio alongside HubSpot. Sales team testing.
July - Sept
Full migration. Customer.io setup. Live chat decision. Claude integration phases 2-4.
Oct - Nov
Burn-in period.
December
HubSpot contract expires. Clean cutover.

The Uncomfortable Question

Is the sales team's underperformance primarily a tooling problem, or does tooling just make an existing problem more visible? Better CRM data will help, but if the issue is also process, training, or targeting strategy, new tools alone won't fix it.

The demo should surface this. If we show the automations, views, and data, and the sales team still wouldn't know what to do differently, the problem isn't HubSpot.

Attio API Reference

Learned by trial and error against the live API (25 Feb 2026). Treat as ground truth over the Attio docs, which are inconsistent in places.

Basics

Object and attribute creation

Creating attributes - POST /v2/objects/{object_slug}/attributes:

Body must be wrapped in data with all fields present:

{
  "data": {
    "title": "Loan Amount",
    "description": "Requested loan amount",
    "api_slug": "loan_amount",
    "type": "currency",
    "is_required": false,
    "is_unique": false,
    "is_multiselect": false,
    "config": {"currency": {"default_currency_code": "GBP", "display_type": "symbol"}}
  }
}

Select and status options

select attributes require options created before use. Setting a value to a non-existent option returns an error.

Create select options: POST /v2/objects/{object}/attributes/{attribute_id}/options

{"data": {"title": "Active"}}

status attributes use POST /v2/objects/{object}/attributes/{attribute_id}/statuses:

{"data": {"title": "DIP Submitted", "target_archive_state": "active"}}

target_archive_state options: "active", "archived-win", "archived-loss". Setting a status on a record requires the status UUID.

Creating records

POST /v2/objects/{object_slug}/records:

{"data": {"values": {"name": "Sarah Mitchell", "fca_number": "SM001"}}}

Value formats by type:

Querying records

POST /v2/objects/{object_slug}/records/query. Filter syntax uses attribute slugs as keys:

{"filter": {"reference": "GH-2026-003"}}
{"filter": {"property_postcode": {"value": {"$contains": "E"}}}}
{"filter": {"loan_amount": {"currency_value": {"$gt": 400000}}}}
{"filter": {"broker": {"target_record_id": {"$eq": "broker-uuid"}}}}

Sorting:

{"sorts": [{"attribute": "loan_amount", "field": "currency_value", "direction": "desc"}]}

Reading values from results - all values are arrays:

record["values"]["name"][0]["value"]                    # text, number
record["values"]["loan_amount"][0]["currency_value"]    # currency
record["values"]["broker"][0]["target_record_id"]       # record-reference
record["values"]["submitted_date"][0]["value"]          # date

Updating records

PATCH /v2/objects/{object_slug}/records/{record_id}:

{"data": {"values": {"bdm_owner": "Alex Turner", "days_in_stage": 5}}}

Notes and Tasks

Notes: POST /v2/notes - requires format field ("plaintext" or "markdown"). Field is content, not content_plaintext. Notes appear on record timeline.

Tasks: POST /v2/tasks - requires format, assignees (can be empty array), and linked_records.

API limitations

Cannot do: create custom objects, create views/kanban, create workflows, create sequences, create Lists (attempted, payload rejected), create email-address/domain attributes, trigger enrichment.

Untested but available: webhooks (POST /v2/webhooks), assert/upsert (POST /v2/objects/{slug}/records/assert), comments/threads, bulk operations (25 writes/sec = ~90k records/hour).

Shell gotcha

API key corrupted by shell interpolation in bash/curl (especially heredocs). Symptom: 401 Unauthorized with "wrong length". Fix: read key from file in Python.