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.
Validated by Sara (26 Feb)
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.
| Function | Current | Target | Status |
|---|---|---|---|
| CRM | HubSpot | Attio (Pro, £68/user/month) | Eval complete, building demos |
| Email marketing | HubSpot | Customer.io | Not started, depends on CRM decision |
| Live chat | HubSpot | Help Scout or Intercom | Not started, lowest priority |
| Object | Key attributes | Relationships |
|---|---|---|
| Brokers | Name, phone, FCA number, activity status, cases 90d, total lending 90d, BDM owner | → Brokerage |
| Brokerages | Name, FCA number, firm type, active brokers, total cases 90d, BDM owner | → Network, → Club |
| Networks | Name | → Parent Network |
| Clubs | Name | (standalone) |
| Cases | Reference, applicant name, property address, loan amount, LTV, stage, submitted date | → Broker, → Brokerage |
| Loans | Account number, borrower name, loan amount, interest rate, maturity date | → Broker, → Brokerage, → Originating Case |
Each object's Record Text (Workspace settings > Objects > Appearance) must be set to the human-readable field. Without this, the UI shows UUIDs everywhere.
| Object | Record Text field |
|---|---|
| Brokers | Name |
| Brokerages | Name |
| Networks | Name |
| Clubs | Name |
| Cases | Reference |
| Loans | Account Number |
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.
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.
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.
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).
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.
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.
Fully structured and queryable, but records don't appear in the broker's native activity timeline. BDM has to look in two places.
Appears in timeline but nothing is queryable. Can't answer "how many meetings this month?" Free text, consistency depends on discipline.
Aggregate reporting works ("which brokers haven't been visited in 3 months?") but no per-interaction detail.
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.
| Approach | Structured per interaction | In timeline | Queryable | Effort |
|---|---|---|---|---|
| A: Custom object only | Yes | No | Yes | Medium |
| B: Note templates only | No | Yes | No | Low |
| C: Hybrid (aggregates only) | Partial | Yes | Partial | Low-medium |
| D: Custom object + mirrored note | Yes | Yes | Yes | Medium |
Air Call is Gen H's exclusive telephony platform. Every broker call goes through it. This is core infrastructure, not a nice-to-have.
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.
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.
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:
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 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.
Auto-generated tasks at every stage transition and time threshold. The BDM's task list IS their daily plan.
| Trigger | Auto-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." |
This is where CRM creates real edge. Four broker segments, each with different data requirements:
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 todayCompare 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 dataBrokers 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 identifiedTrack "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| Trigger | Action | Outcome |
|---|---|---|
| Case stage changes | Reset days-in-stage counter, update last-stage-change date | Clean pipeline tracking |
| Daily schedule (6am) | Find stuck cases, create prioritised tasks | Morning brief without AI |
| New case created | Auto-populate brokerage and network from broker record | Data integrity |
| Broker's 3rd case created | Flip status from "New" to "Active", notify BDM | Tracks development |
| Case completes | Recalculate broker totals (completions, lending, last date) | Scorecard stays current |
| Broker added to Watch List | Broadcast alert to BDM | Risk pushed to them |
| Off-panel broker submits | Flag on case + alert: "Off-panel at [brokerage]. Handle with care." | Risks surfaced in context |
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):
| Trigger | Sequence | Sends from | Steps |
|---|---|---|---|
| First case submitted | Welcome | BDM | Day 0: Thanks + what to expect. Day 3: Case update + our process. Day 7: How was the experience? |
| Case completes | Nurture | BDM | Day 0: Congratulations. Day 14: Remortgage pipeline? Day 30: What's new at Gen H. |
| DIP submitted, no response 3d | Nudge | BDM | Day 3: Need any help with this case? Day 7: Here's what we can do. |
| 30 days inactive | Win-back | Marketing (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 list | Pre-event warm-up | Marketing | Day -7: Looking forward to it. Day -1: Find us at stand X. Day +2: Follow-up on what we discussed. |
Attio proves its value standalone first. Claude is the accelerant, layered in after trust is established.
/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.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.
What to build now to demonstrate value to the sales team.
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."
mcp.attio.com/mcp) - does it support read and write? What auth flow is required?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.
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.
https://api.attio.com/v2/Authorization: Bearer <api_key> header. Key generated in Settings > Developers.urllib. Community TypeScript SDK (npm add attio-js) untested.mcp.attio.com/mcp (OAuth), community attio-mcp on npm (API key).api_slug from their name (e.g. "Brokerages" → brokerages). Use GET /v2/objects to discover slugs.record_id, created_at, created_by.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"}}
}
}
config is required even if empty ({}). Omitting it causes a validation error.record-reference types, config: {"record_reference": {"allowed_objects": ["target_slug"]}}. Slugs work, not just UUIDs.email-address and domain types (system-only on People/Companies). Use text as workaround on custom objects.text, number, currency, date, select, status, phone-number, record-reference.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.
POST /v2/objects/{object_slug}/records:
{"data": {"values": {"name": "Sarah Mitchell", "fca_number": "SM001"}}}
Value formats by type:
text: "name": "Sarah Mitchell"number: "ltv": 75currency: plain number, not object. "loan_amount": 350000 works. {"currency_code": "GBP", "value": 350000} does NOT.date: "submitted_date": "2026-02-10"select: string matching option titlestatus: "case_stage": [{"status": "uuid"}]record-reference: "broker": [{"target_object": "brokers", "target_record_id": "uuid"}]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
PATCH /v2/objects/{object_slug}/records/{record_id}:
{"data": {"values": {"bdm_owner": "Alex Turner", "days_in_stage": 5}}}
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.
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).
API key corrupted by shell interpolation in bash/curl (especially heredocs). Symptom: 401 Unauthorized with "wrong length". Fix: read key from file in Python.