Skip to main content
← Back to Blog

Build a personalization writer that actually sounds human

Colin Gillingham··4 min read
gtm-automationhubspotai-agentssales-automationn8nemail-personalization

This post is part of the GTM Automation Playbook — a 13-part series on building AI-powered GTM agents with HubSpot.


Only 5% of B2B senders personalize every email. The other 95% blast templates and wonder why reply rates sit at 3-5%. Personalized outbound gets roughly 2x the responses. The gap isn't knowledge. It's labor. Writing a custom opener for every prospect takes 5-10 minutes per contact, and your SDR has 80 to work through by Friday.

That math doesn't work. So you automate it.

The four-node workflow

I use n8n for this because the architecture is dead simple: trigger, data pull, LLM call, write-back. No custom code required. The whole thing runs in four nodes.

Node 1: HubSpot Trigger. Use n8n's HubSpot Trigger node, set to fire on "Contact Created." This kicks off the workflow every time a new contact enters your CRM. If you'd rather run in batches, swap this for a Schedule Trigger that polls a HubSpot list every 15 minutes using the search endpoint (POST /crm/v3/objects/contacts/search).

Node 2: Pull context. Add a HubSpot node to fetch the contact's properties: firstname, lastname, jobtitle, company, industry. Then hit the Companies API (GET /crm/v3/objects/companies/{companyId}) via an HTTP Request node to grab company size, funding stage, and description. The more context you feed the model, the better the output. But diminishing returns kick in fast. Job title + company + industry gets you 80% of the way there.

Node 3: Generate the opener. Connect a Basic LLM Chain node with an Anthropic Chat Model sub-node. Use Claude Sonnet for the best cost-to-quality ratio on email generation. Set temperature to 0.7. Your system prompt matters more than anything else in this workflow. Here's what works:

You are writing a personalized cold email opening paragraph.
Given prospect and company data, write 3-5 sentences.

Rules:
- Reference one specific detail about the prospect or their company
- Connect it to a pain point our product solves
- Sound like a human, not a marketing team
- No "I hope this finds you well" or "I came across your profile"
- End with a question, not a calendar link

Our product: [YOUR VALUE PROP]
Target pain: [SPECIFIC PROBLEM YOU SOLVE]

Prospect: {{ $json.firstname }} {{ $json.lastname }}
Title: {{ $json.jobtitle }}
Company: {{ $json.company }}
Industry: {{ $json.industry }}
Company description: {{ $json.company_description }}

Skip the AI Agent node here. You don't need tool-calling or multi-step reasoning for email generation. The Basic LLM Chain is faster and cheaper for a single prompt-in, text-out task.

Node 4: Write it back. Two options. Option A: create a custom text property in HubSpot called ai_personalized_opener and update the contact via PATCH /crm/v3/objects/contacts/{contactId}. Option B: create a note on the contact using POST /crm/v3/objects/notes with hs_note_body set to the generated text and an association to the contact (associationTypeId: 202). I prefer the custom property. It's easier to reference in sequences and workflows downstream.

What good output looks like

Bad AI personalization: "I noticed your company is doing great things in the SaaS space. I'd love to show you how we can help."

Good AI personalization: "Saw that Acme just expanded their SDR team to 12. When teams scale that fast, the gap between top performers and everyone else usually widens. We've been working with similar B2B teams to standardize what the best reps do differently."

The difference is specificity. The model needs at least one concrete detail to anchor the email. Company name alone isn't enough. Job title + industry + one company fact (size, funding, hiring) gives it enough to write something a human would actually send.

The gotchas nobody mentions

Rate limits. HubSpot's API allows 100 requests per 10 seconds on most plans. If you're processing a big list, add a Wait node between batches or use the batch read endpoint (POST /crm/v3/objects/contacts/batch/read) to pull 100 contacts at once.

Prompt drift. Your first prompt will need tuning. Run 20 contacts through, read every output, and adjust. Common fixes: adding "never mention AI" to the rules, specifying your product's actual language instead of letting the model guess, and constraining length to 3 sentences instead of 5.

The Anthropic thinking bug. If you're using the n8n Anthropic Chat Model sub-node, disable "Enable Thinking" in the node settings. There's a known compatibility issue with tool-calling when thinking is enabled. For the Basic LLM Chain this shouldn't matter, but save yourself the debugging.

Where this fits in your stack

This workflow replaces the manual part of outbound personalization. It doesn't replace your sequence tool, your sending infrastructure, or your rep's judgment. The output goes into a HubSpot property. Your rep reviews it, tweaks if needed, and drops it into their sequence.

The compound effect is what matters. A team of 5 SDRs personalizing 50 emails a day used to mean 250 instances of someone staring at LinkedIn for 5 minutes. Now it means 250 custom openers generated in under 10 minutes, with the rep spending their time on the ones that need a human touch.

n8n has community templates for this exact pattern. Search for "Email Outreach Drafter Based on HubSpot Data" in their template library. It's a solid starting point you can modify with your own prompt and property mappings.

Colin Gillingham

Need a Fractional Head of AI?

I help companies build an AI operating system — shared context across teams, AI handling the repetitive work, and your people focused on what actually matters.

15+

Years in Tech

12+

AI Products Shipped

3

Fortune 500 Brands