Agent skill
hubspot-core-workflow-a
Build a complete HubSpot CRM contact-to-deal pipeline workflow. Use when implementing lead capture, contact management, deal creation, or end-to-end sales pipeline automation with HubSpot. Trigger with phrases like "hubspot sales pipeline", "hubspot lead workflow", "hubspot contact to deal", "hubspot CRM automation", "hubspot pipeline".
Install this agent skill to your Project
npx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/main/plugins/saas-packs/hubspot-pack/skills/hubspot-core-workflow-a
SKILL.md
HubSpot Core Workflow A: Contact-to-Deal Pipeline
Overview
End-to-end workflow: capture a lead, create/update contact, create company, create deal in pipeline, advance deal stages, and log activities. The primary money-path workflow for HubSpot CRM.
Prerequisites
- Completed
hubspot-install-authsetup - Scopes:
crm.objects.contacts.write,crm.objects.companies.write,crm.objects.deals.write - Understanding of your HubSpot deal pipeline and stages
Instructions
Step 1: Capture and Upsert Contact
import * as hubspot from '@hubspot/api-client';
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: 3,
});
interface LeadInput {
email: string;
firstName: string;
lastName: string;
company: string;
phone?: string;
source?: string;
}
async function upsertContact(lead: LeadInput): Promise<string> {
// Search for existing contact by email
// POST /crm/v3/objects/contacts/search
const existing = await client.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{ propertyName: 'email', operator: 'EQ', value: lead.email }],
}],
properties: ['firstname', 'lastname', 'email'],
limit: 1,
after: 0,
sorts: [],
});
if (existing.results.length > 0) {
// Update existing contact
const contactId = existing.results[0].id;
await client.crm.contacts.basicApi.update(contactId, {
properties: {
firstname: lead.firstName,
lastname: lead.lastName,
phone: lead.phone || '',
hs_lead_status: 'NEW',
},
});
console.log(`Updated existing contact: ${contactId}`);
return contactId;
}
// Create new contact
const contact = await client.crm.contacts.basicApi.create({
properties: {
email: lead.email,
firstname: lead.firstName,
lastname: lead.lastName,
company: lead.company,
phone: lead.phone || '',
lifecyclestage: 'lead',
hs_lead_status: 'NEW',
},
associations: [],
});
console.log(`Created new contact: ${contact.id}`);
return contact.id;
}
Step 2: Find or Create Company
async function findOrCreateCompany(
domain: string,
name: string
): Promise<string> {
// Search by domain
const existing = await client.crm.companies.searchApi.doSearch({
filterGroups: [{
filters: [{ propertyName: 'domain', operator: 'EQ', value: domain }],
}],
properties: ['name', 'domain'],
limit: 1,
after: 0,
sorts: [],
});
if (existing.results.length > 0) {
return existing.results[0].id;
}
const company = await client.crm.companies.basicApi.create({
properties: { name, domain },
associations: [],
});
return company.id;
}
Step 3: Create Deal in Pipeline
async function createDeal(
contactId: string,
companyId: string,
dealName: string,
amount: number
): Promise<string> {
// First, get pipeline stages to find the right stage ID
// GET /crm/v3/pipelines/deals
const pipelines = await client.crm.pipelines.pipelinesApi.getAll('deals');
const defaultPipeline = pipelines.results.find(p => p.label === 'Sales Pipeline')
|| pipelines.results[0];
const firstStage = defaultPipeline.stages.sort(
(a, b) => Number(a.displayOrder) - Number(b.displayOrder)
)[0];
// POST /crm/v3/objects/deals
const deal = await client.crm.deals.basicApi.create({
properties: {
dealname: dealName,
amount: String(amount),
pipeline: defaultPipeline.id,
dealstage: firstStage.id,
closedate: new Date(Date.now() + 30 * 86400000).toISOString(),
},
associations: [
{
to: { id: contactId },
types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 3 }],
},
{
to: { id: companyId },
types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 5 }],
},
],
});
console.log(`Created deal: ${deal.id} in stage "${firstStage.label}"`);
return deal.id;
}
Step 4: Advance Deal Stage
async function advanceDealStage(dealId: string, stageName: string): Promise<void> {
// Look up stage ID from pipeline
const deal = await client.crm.deals.basicApi.getById(dealId, ['pipeline', 'dealstage']);
const pipelines = await client.crm.pipelines.pipelinesApi.getAll('deals');
const pipeline = pipelines.results.find(p => p.id === deal.properties.pipeline);
const targetStage = pipeline?.stages.find(s => s.label === stageName);
if (!targetStage) {
throw new Error(`Stage "${stageName}" not found in pipeline "${pipeline?.label}"`);
}
await client.crm.deals.basicApi.update(dealId, {
properties: { dealstage: targetStage.id },
});
console.log(`Deal ${dealId} moved to "${stageName}"`);
}
Step 5: Log Activity (Note)
async function logNote(contactId: string, dealId: string, body: string): Promise<void> {
// POST /crm/v3/objects/notes
await client.crm.objects.notes.basicApi.create({
properties: {
hs_note_body: body,
hs_timestamp: new Date().toISOString(),
},
associations: [
{
to: { id: contactId },
types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 202 }],
},
{
to: { id: dealId },
types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 214 }],
},
],
});
}
Complete Pipeline Example
async function processLead(lead: LeadInput) {
const contactId = await upsertContact(lead);
const domain = lead.email.split('@')[1];
const companyId = await findOrCreateCompany(domain, lead.company);
// Associate contact with company
await client.crm.associations.v4.basicApi.create(
'contacts', contactId, 'companies', companyId,
[{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 1 }]
);
const dealId = await createDeal(
contactId, companyId,
`${lead.company} - New Opportunity`,
10000
);
await logNote(contactId, dealId, `Lead captured from ${lead.source || 'website'}`);
return { contactId, companyId, dealId };
}
Output
- Contact upserted (create or update based on email)
- Company found or created by domain
- Deal created in the correct pipeline stage with associations
- Deal stage advanced through the pipeline
- Activity note logged on contact and deal
Error Handling
| Error | Code | Cause | Solution |
|---|---|---|---|
409 Conflict |
409 | Duplicate email on create | Use search-then-create pattern above |
PIPELINE_STAGE_NOT_FOUND |
400 | Invalid stage ID | Fetch pipeline stages first |
INVALID_ASSOCIATION_TYPE |
400 | Wrong associationTypeId |
Check default association types |
PROPERTY_DOESNT_EXIST |
400 | Custom property not created | Create in HubSpot Settings > Properties |
Resources
Next Steps
For marketing automation workflows, see hubspot-core-workflow-b.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
dockerfile-generator
Dockerfile Generator - Auto-activating skill for DevOps Basics. Triggers on: dockerfile generator, dockerfile generator Part of the DevOps Basics skill category.
branch-naming-helper
Branch Naming Helper - Auto-activating skill for DevOps Basics. Triggers on: branch naming helper, branch naming helper Part of the DevOps Basics skill category.
readme-generator
Readme Generator - Auto-activating skill for DevOps Basics. Triggers on: readme generator, readme generator Part of the DevOps Basics skill category.
makefile-generator
Makefile Generator - Auto-activating skill for DevOps Basics. Triggers on: makefile generator, makefile generator Part of the DevOps Basics skill category.
gitignore-generator
Gitignore Generator - Auto-activating skill for DevOps Basics. Triggers on: gitignore generator, gitignore generator Part of the DevOps Basics skill category.
pre-commit-hook-setup
Pre Commit Hook Setup - Auto-activating skill for DevOps Basics. Triggers on: pre commit hook setup, pre commit hook setup Part of the DevOps Basics skill category.
Didn't find tool you were looking for?