meeting.creation events with the contact and company IDs in the payload, so the workflow does not need a polling loop.
We tried the same pattern with Claude Haiku — same input, slightly cheaper, but the brief writing was visibly less polished. We kept GPT-4o for this single use case. A recent r/sales thread on AI sales prep shows the same drift across teams — Haiku for classification, GPT-4o for narrative writing.
## The 4-block architecture
{
"parameters": {
"httpMethod": "POST",
"path": "hubspot-meeting-created",
"responseMode": "lastNode",
"options": {
"rawBody": true
}
},
"name": "HubSpot meeting webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [240, 300]
}
The webhook URL is registered in HubSpot's developer console under "Webhooks", subscribed to the meeting.creation event. HubSpot signs every webhook with HMAC-SHA256 — we verify the signature in a follow-up Code node using the X-HubSpot-Signature-v3 header and the app's client secret. Without that check, anyone who guesses the URL can spam the workflow. We added the check on day 1 after a friend's penetration tester pointed it out.
### Node 2 — HubSpot contact + company + engagements (parallel HTTP)
{
"parameters": {
"method": "GET",
"url": "=https://api.hubapi.com/crm/v3/objects/contacts/{{ $json.contactId }}?associations=companies&properties=firstname,lastname,email,jobtitle,lifecyclestage,hubspot_owner_id",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "hubspotApi"
},
"name": "HubSpot contact",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [460, 200]
}
This is one of three parallel calls. The other two hit /crm/v3/objects/companies/{companyId} and /crm/v3/objects/companies/{companyId}/associations/engagements. Run them in parallel via three branches off the webhook node, then Merge by Position. Total round-trip is under 800 ms even from a Hetzner box in Frankfurt to HubSpot's US data centre.
The properties query parameter limits the payload — by default HubSpot returns ~70 contact properties, most of them empty. Asking for only what you need keeps payloads small and the LLM context cheap.
### Node 3 — GPT-4o brief generator (HTTP Request)
{
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Authorization", "value": "=Bearer {{ $credentials.openAiApi.apiKey }}" },
{ "name": "Content-Type", "value": "application/json" }
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"model\": \"gpt-4o\",\n \"max_tokens\": 800,\n \"temperature\": 0.4,\n \"messages\": [\n { \"role\": \"system\", \"content\": \"You are a sales-prep writer for a B2B SaaS team. Output a 1-page brief in this EXACT structure:\\n\\n## Opening hook (1-2 sentences referring to recent news or activity)\\n## Deal context (1 paragraph: what stage, what they care about, last touch)\\n## 3 questions to ask\\n1. ...\\n2. ...\\n3. ...\\n## 3 landmines to avoid\\n- ...\\n- ...\\n- ...\\n## Suggested next step\\n(1 sentence)\\n\\nRules: every claim cites the source field (HubSpot/News/etc). No marketing fluff. Imperative tone.\" },\n { \"role\": \"user\", \"content\": \"=Contact: {{ JSON.stringify($('HubSpot contact').item.json) }}\\n\\nCompany: {{ JSON.stringify($('HubSpot company').item.json) }}\\n\\nLast 90 days engagements: {{ JSON.stringify($('Engagements').item.json) }}\\n\\nLast 14 days news: {{ JSON.stringify($('NewsAPI').item.json.articles) }}\\n\\nWrite the brief.\" }\n ]\n}"
},
"name": "GPT-4o brief",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [900, 300]
}
Three things matter in this node. The system prompt locks the structure — every brief looks the same, which is why reps actually read them (they know exactly where to scan). Temperature 0.4 — high enough for natural writing, low enough that the same input does not produce wildly different briefs. max_tokens 800 — caps the cost at roughly ₹1.40 per call regardless of how chatty the model wants to be.
- 1. Webhook trigger (HubSpot meeting.creation)
- 2. Code — verify HMAC signature, reject if invalid
- 3. HTTP — HubSpot contact (parallel)
- 4. HTTP — HubSpot company (parallel)
- 5. HTTP — HubSpot engagements last 90d (parallel)
- 6. Merge — wait for all three
- 7. HTTP — NewsAPI for company name, last 14d
- 8. HTTP — GPT-4o chat completion with structured prompt
- 9. Code — extract content, format markdown to HTML
- 10. HTTP — HubSpot owner email lookup (so we email the right rep)
- 11. Email Send — to rep's inbox
- 12. HTTP — HubSpot create note (attach brief to the contact record)
- 13. Error workflow — fallback "manual prep needed" email + Slack alert to ops
- HubSpot Private App created with scopes crm.objects.contacts.read, crm.objects.companies.read, crm.objects.engagements.read, crm.objects.notes.write
- Webhook URL registered in HubSpot developer console for meeting.creation event
- HMAC signature verification step in workflow (X-HubSpot-Signature-v3 header)
- OpenAI API key stored as n8n credential, monthly cap set on the OpenAI dashboard
- NewsAPI key stored as n8n credential, daily quota monitored
- Email Send node configured with SMTP credentials (we use a Postmark SMTP relay)
- Test webhook fired with a real HubSpot meeting; brief lands in inbox within 120 seconds
- Fallback "manual prep needed" template tested by simulating GPT-4o failure
- HubSpot Note created on the contact for every brief (audit trail)
- Workflow exported to git as JSON the day of go-live
## Opening hook
[1-2 sentences citing news or HubSpot activity]
## Deal context
[1 paragraph: stage, MRR target, last touch, owner notes]
## 3 questions to ask
1. [open-ended, references their world]
2. [diagnostic, narrows pain]
3. [advance, sets next step]
## 3 landmines to avoid
- [pricing topic, competitive mention, etc]
- [...]
- [...]
## Suggested next step
[one sentence]
Reps told us in feedback that the "3 landmines" section was what made them actually read the brief — it was the only AI-generated content that warned them about something instead of just describing the prospect. We kept that section in even when GPT had nothing useful to say there ("no obvious landmines based on history" is a valid output).
## Common mistakes — symptoms first
Symptom: "Brief takes 3-4 minutes to arrive, not 90 seconds." Cause: HubSpot calls are sequential, not parallel. Fix: branch the webhook node into 3 parallel HTTP nodes for contact + company + engagements, then Merge by Position. Cuts 60% of the latency.
Symptom: "Briefs hallucinate facts that are not in HubSpot or News." Cause: prompt does not enforce source citation. Fix: add "every claim cites the source field (HubSpot/News/etc)" to the system prompt. Hallucinations dropped from ~5% to under 1% in our measurements.
Symptom: "Reps complain the briefs are 'too generic' for accounts they know well." Cause: the contact record has rich notes from prior reps that we are not pulling. Fix: include the last 5 HubSpot Notes in the engagement payload. Specificity went up dramatically.
Symptom: "Webhook fires twice for the same meeting." Cause: HubSpot retries on a slow response. Fix: respond 200 immediately from n8n's webhook (use Respond to Webhook node) before doing the heavy work. Run the rest of the workflow async via Execute Workflow. We added this in week 2.
Symptom: "GPT-4o costs 3x what we expected." Cause: someone forgot the max_tokens cap and the model wrote essays. Fix: hard cap at 800. Also drop temperature to 0.4 — higher temp tends to produce verbose briefs.
## Mini case study — 5 weeks at the Bengaluru SaaS team
The first install ran on 1 September 2025. After 5 weeks: 1,420 briefs generated, 89% open rate (vs 31% on the old hand-written wiki page), avg time-from-meeting-booked-to-brief-delivered was 87 seconds, 6 briefs failed (3 NewsAPI rate limits, 2 GPT timeouts, 1 HubSpot 503) — all 6 fell back to the "manual prep needed" template and the rep received a Slack ping. Sales VP reported in the monthly review that prep-time per call dropped from 25 to 4 minutes (just reading the brief).
For the broader pattern of LLM-assisted prep, see our writeup of Slack + Linear bug triage — same architecture (event → CRM/source pull → LLM → output channel), different domain.
## When NOT to build this
Skip this if (a) your team has under 5 reps — the per-rep ROI on automating prep is not there, (b) your sales motion is highly relationship-driven and reps prep from memory of past calls — automation undermines the muscle, or (c) your CRM is sparse (most contacts have <5 fields filled in) — garbage in, garbage out applies hard to LLMs. We turned down one client in 2025 for reason (c). They needed CRM hygiene before any of this would help.
For a different LLM-pre-flight pattern in a different vertical, see our TalkDrill case — same prompt-template-driven approach, applied to interview prep instead of sales prep.
## FAQ
### How long to build and deploy this n8n + HubSpot + ChatGPT brief flow?
For us, 8 working days end-to-end: 1 day on HubSpot Private App + scopes, 2 days on the n8n flow (HMAC verification, parallel HTTP, Merge), 2 days on prompt iteration with the sales VP, 1 day on the email template, 2 days on the 5-week shadow run with one rep before rolling out to the full team.
### Can I use Claude Sonnet 4.5 instead of GPT-4o?
Yes. Output quality is comparable; cost is similar. We landed on GPT-4o for narrative briefs because the writing felt slightly more polished in side-by-side reviews. For pure classification we use Claude Haiku (see our bug-triage post) — different model for different jobs.
### How do I prevent the brief from leaking confidential info to the LLM?
Two layers. First, the HubSpot scope is read-only on contacts/companies/engagements — no financial fields, no internal notes flagged "private". Second, the system prompt explicitly states "do not echo any field marked private". For high-sensitivity accounts we route to a separate workflow that only includes public information.
### What if HubSpot does not have an email or company for the contact?
The brief generator handles missing fields gracefully — the prompt instructs the model to say "no email on file" or "company unknown" instead of hallucinating. The Code node before the prompt fills missing fields with the literal string "MISSING" so the model knows.
### Why NewsAPI and not Google News scraping?
Google News blocks scraping aggressively and the legal risk is non-zero. NewsAPI's Developer plan ($0/mo for 100 requests/day) is enough for 280 briefs/month with caching. For higher volume we would move to a paid plan or a service like Diffbot.
### Can the brief include the prospect's recent LinkedIn activity?
Not in the version we ship — LinkedIn's terms-of-service forbid third-party scraping for sales enablement. We considered using LinkedIn's official Sales Navigator API but the cost is prohibitive at this team's size. Reps still do a 30-second LinkedIn check manually.
### How do I handle a prospect that does not have any news in 14 days?
The prompt handles this. It produces an opening hook based on HubSpot activity instead — last touch, deal stage change, recent visit to the website. We tested 200 such cases; the briefs read fine.
Want this sales-prep automation built?
We ship the HubSpot + ChatGPT auto-brief flow — n8n on your Hetzner box, HubSpot Private App scopes, prompt iteration with your sales lead, email template tuned to your team's tone — in 8 working days for ₹54,000. Suitable for any 5+ rep B2B sales team using HubSpot or Salesforce.
Book a 20-min Call
