Agent builder
Goal: let an agent you build in Agent Factory use HubSpot through MCP tools.Before an agent can call the connector, a Workspace builder must have installed and configured the HubSpot app in a workspace (see the Workspace builder tab), and — for the central OAuth mode — a Platform admin must have provisioned the shared OAuth client (see the Platform admin setup accordion above).
This connector runs in the tenant app-instance context: your agent is authorized two ways at once — it is identified by the agent_id that Agent Factory injects through the capability Scope, and that agent must appear in the connector’s authorized-agents allowlist (managed in the configuration app). The HubSpot access token itself is resolved server-side from the configured auth mode.There are two ways to wire it up. Pick based on how much isolation you need.Option A — Enable the shared capability from the catalog
The fastest path: a Platform admin has already published a HubSpot capability to the Capabilities catalog (see the Platform admin setup accordion above — the Add to catalog button, or §2), so you just pick it from the catalog.Open your agent in Agent Factory
Open the agent you want to extend and go to its capabilities / tools.
Add the HubSpot capability
Browse the capability catalog, select HubSpot, and enable it. The MCP endpoint URL and the Scope (context_id,agent_id,user_id) are already wired by the admin — nothing to paste, and the shared instance accepts every agent, so there is no allowlist step on your side.
Connect a HubSpot account
On the first tool call, an unconnected user is prompted to sign in — Agent Factory surfaces a connect_url to HubSpot’s authorization page, where the user picks the portal to authorize (from the allowlisted set) and reviews the requested scopes. After sign-in the per-user token is stored and reused on subsequent calls.
Convenient, but your agent runs against a shared, platform-managed instance: its HubSpot credentials are owned by someone else and the instance accepts every agent that is granted the capability. Prefer Option B for anything beyond quick experiments.
Option B — Run it from your own workspace (recommended)
For production agents, install the connector in your own workspace and point the agent at that workspace’s MCP endpoint.Prefer this mode for security. Because the MCP runs in your app-instance context, the HubSpot credentials, the per-user OAuth tokens and the authorized-agents allowlist are all scoped to your workspace — not shared platform-wide. You decide exactly which agents may call it and which HubSpot account / auth mode backs them, and a misconfiguration elsewhere can never expose your data. The shared catalog capability (Option A) is a broad surface many agents can reach; your own workspace is an isolated, least-privilege boundary.
Install and configure the connector in your workspace
Follow the Workspace builder tab: install HubSpot in your workspace, open its Configuration app, choose the auth mode and connect a HubSpot account (or paste a Private App token).
Allowlist your agent
In that workspace’s config app, open Authorized agents and tick your agent (the Install capability button does this for you), or enable Allow all agents.
Add the MCP capability to your agent
In your agent, add a capability pointing at your workspace’s MCP Endpoint URL, and set its Scope to:context_id,agent_id,user_id
The agent_id is what lets the connector identify and authorize your agent — without it, every call is rejected with an explicit “agent could not be identified” message. This Scope is separate from the HubSpot OAuth scopes. Connect a HubSpot account
On the first tool call, the user is prompted to sign in (or uses Connect in the config app). The per-user token is stored and reused; refresh is automatic.
Brief the agent in its system prompt
Whichever option you pick, wiring the capability is not enough — the agent must know the connector exists and when to reach for it. Add a short paragraph to the agent’s system prompt. Copy-pasteable starter:You have access to the HubSpot MCP server. Use it whenever the user asks about CRM data — contacts, companies, deals, tickets, engagements, pipelines or marketing assets (forms, emails, workflows). Each tool is an entity that takes an `action` argument. Always prefer `action: search` over `action: list` for filtered lookups. For batch work, use the `batch*` actions instead of looping individual calls. Always confirm with the user before destructive actions (archive, delete, batchArchive, unenrollContact, removeMembers).
Refine the trigger keywords (resource names, business domains, typical user phrasings) so the agent reliably picks up the right intent in your context.Legacy AI Knowledge agents (no native MCP picker): add the connector under Advanced > Tools > MCP and paste the MCP Endpoint URL. The agent still has to be allowlisted in the config app and its identity propagated so the connector can read its agent_id.
Restricting to read-only (least privilege). HubSpot tools cover both reads and writes (create/update contacts, companies, deals). The requested OAuth scopes are the grant. To allow only read access, set the Scopes field in the configuration app to the read-only set, dropping every *.write scope, e.g.:oauth crm.objects.contacts.read crm.objects.companies.read crm.objects.deals.read crm.objects.owners.read tickets
With central OAuth (oauthCentral) you do not create your own HubSpot app — keep oauthCentral and enter the read scopes; your tenant scope overrides the platform default (the central app must list these read scopes). Write calls are then rejected by HubSpot with 403. The scope is set at the workspace level (a workspace editor can widen it again). The MCP server exposes 18 entity-level tools, each with an action argument to select the operation. Every tool accepts an outputFormat parameter (see Output Formats).These three CRM tools share the same set of actions.| Action | Description |
|---|
list | List records with pagination and property selection |
get | Retrieve a single record by id (or by idProperty) |
create | Create a record with properties and optional associations |
update | Update a record by id |
archive | Archive (soft-delete) a record |
search | Full-text and structured search with filter groups, sorts and a query shortcut |
merge | Merge two records into one (primaryObjectId ← objectIdToMerge) |
batchRead | Read up to 100 records at once |
batchCreate | Create up to 100 records in one call |
batchUpdate | Update up to 100 records in one call |
batchArchive | Archive up to 100 records in one call |
tickets
Same actions as the CRM trio above, minus merge.associations
| Action | Description |
|---|
list | List associations from a record to a target object type |
create | Create an association between two records |
archive | Remove an association between two records |
notes / tasks / calls / engagementEmails / meetings
All five engagement tools share the same actions.| Action | Description |
|---|
list | List engagements |
get | Retrieve a single engagement |
create | Create an engagement, optionally with associations to CRM records |
update | Update an engagement |
archive | Archive an engagement |
pipelines
| Action | Description |
|---|
list | List pipelines for an object type |
get | Retrieve a pipeline |
create | Create a pipeline |
update | Update a pipeline |
archive | Archive a pipeline |
listStages | List the stages of a pipeline |
getStage | Retrieve a stage |
createStage | Add a stage to a pipeline |
updateStage | Update a stage |
archiveStage | Archive a stage |
properties
| Action | Description |
|---|
list | List properties for an object type |
get | Retrieve a property |
create | Create a property |
update | Update a property |
archive | Archive a property |
listGroups | List property groups for an object type |
createGroup | Create a property group |
updateGroup | Update a property group |
archiveGroup | Archive a property group |
owners
| Action | Description |
|---|
list | List owners (optionally filter by email) |
get | Retrieve an owner by id |
lists
| Action | Description |
|---|
create | Create a list |
get | Retrieve a list (optionally with filters) |
update | Rename a list |
delete | Delete a list |
search | Search lists by name and processing type |
addMembers | Add records to a static list |
removeMembers | Remove records from a static list |
getMemberships | List the members of a list |
| Action | Description |
|---|
list | List marketing forms |
get | Retrieve a form |
create | Create a form |
update | Update a form |
archive | Archive a form |
submit | Submit values to a form on behalf of a visitor |
marketingEmails
| Action | Description |
|---|
list | List marketing emails |
get | Retrieve a marketing email |
create | Create a marketing email |
update | Update a marketing email |
archive | Archive a marketing email |
sendTransactional | Send a transactional single-send email to a recipient |
workflows
| Action | Description |
|---|
list | List workflows |
get | Retrieve a workflow |
enrollContact | Enroll a contact in a workflow by email |
unenrollContact | Unenroll a contact from a workflow by email |
files
| Action | Description |
|---|
list | List files |
get | Retrieve file metadata |
getSignedUrl | Create a signed URL to download a file |
archive | Archive a file |
importFromUrl | Import a file from a public URL |
listFolders | List folders |
createFolder | Create a folder |
archiveFolder | Archive a folder |
Every tool accepts an outputFormat parameter that controls the MCP response shape:
verbose (default) — human-readable text for LLM consumption
structured — machine-readable JSON in structuredContent
both — both text and structured content
{
"name": "contacts",
"arguments": {
"action": "search",
"filterGroups": [
{
"filters": [
{ "propertyName": "email", "operator": "EQ", "value": "alice@acme.com" }
]
}
],
"properties": ["email", "firstname", "lastname", "lifecyclestage"],
"limit": 1
}
}
| Parameter | Required | Description |
|---|
action | Yes | Must be search |
filterGroups | No | Array of { filters: [{ propertyName, operator, value }] }. Filters inside a group are AND’d; groups are OR’d |
sorts | No | Array of { propertyName, direction } |
query | No | Free-text query alternative to filterGroups |
properties | No | Properties to include in the response |
limit | No | Page size (max 100) |
after | No | Cursor from a previous response (paging.next.after) |
deals (action: create)
{
"name": "deals",
"arguments": {
"action": "create",
"properties": {
"dealname": "Acme — annual contract",
"amount": "120000",
"dealstage": "appointmentscheduled",
"pipeline": "default",
"closedate": "2026-09-30"
},
"associations": [
{
"to": { "id": "11223344" },
"types": [
{ "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 5 }
]
}
]
}
}
| Parameter | Required | Description |
|---|
action | Yes | Must be create |
properties | Yes | Map of HubSpot deal properties |
associations | No | Optional list of association targets ({ to.id, types: [{ associationCategory, associationTypeId }] }) |
lists (action: addMembers)
{
"name": "lists",
"arguments": {
"action": "addMembers",
"listId": "12345"
}
}
| Parameter | Required | Description |
|---|
action | Yes | Must be addMembers |
listId | Yes | Target list ID (only valid for static / MANUAL lists) |
marketingEmails (action: sendTransactional)
{
"name": "marketingEmails",
"arguments": {
"action": "sendTransactional",
"emailId": "67890",
"message": {
"to": "alice@acme.com",
"from": "noreply@acme.com"
},
"contactProperties": { "firstname": "Alice" },
"customProperties": { "orderId": "ORD-12345" }
}
}
| Parameter | Required | Description |
|---|
action | Yes | Must be sendTransactional |
emailId | Yes | The single-send email template ID |
message | Yes | { to, from, cc, bcc, replyTo, sendId } |
contactProperties | No | Properties to set on the contact for personalization |
customProperties | No | Custom merge tags resolved inside the email body |
{
"name": "workflows",
"arguments": {
"action": "enrollContact",
"workflowId": "98765",
"email": "alice@acme.com"
}
}
| Parameter | Required | Description |
|---|
action | Yes | Must be enrollContact |
workflowId | Yes | Target workflow |
email | Yes | Email of the contact to enroll |
Workspace builder
Goal: install the connector in a workspace, configure authentication and the agent allowlist, and call HubSpot operations from your automations.Installation
- Go to Apps in your workspace
- Search for HubSpot and install it
- Open the Configuration app (the link auto-populated on install) to choose the auth mode, provide credentials, connect a HubSpot account, and allow the agents that may call the connector
Configuration
| Field | Description |
|---|
| Configuration app | Auto-populated on install — open this link to configure authentication (mode + credentials), connect a HubSpot account (OAuth modes), and manage the authorized-agents allowlist. |
The configuration app drives everything; the app instance itself has no per-field credential form. From it you pick one of:| Auth mode | What you provide | Best for |
|---|
oauthCentral | Nothing — just click Connect | Workspaces on a platform where the maintainer set up the central HubSpot client; human-facing agents where each user acts as themselves |
oauth | Your own HubSpot Client ID + Secret (redirect URI shown in the config app) | You manage your own HubSpot OAuth app |
accessToken | A HubSpot Private App access token (PAT) | Service-account / non-interactive automations where one identity is preferable |
Credentials are stored in the workspace’s Secrets (hubspotAuth) and resolved server-side. The agent allowlist (Authorized agents, secret hubspotAuthorizedAgents) gates which Agent Factory agents may call the MCP endpoint — see the Agent builder tab. Builder automations calling Hubspot.op: instructions resolve the same workspace credentials.
Available Instructions
Every instruction resolves credentials from the workspace configuration. CRM list operations accept limit (default 100) and after for cursor pagination, plus properties to request specific fields.| Instruction | Description | Returns |
|---|
listContacts | List contacts; paginate with limit/after, pick fields with properties | { results: [{ id, properties }], paging } |
getContact | Get a contact by contactId (or by idProperty) | { id, properties, associations } |
createContact | Create a contact from properties, optional associations | { id, properties } |
updateContact | Update a contact by contactId with properties | { id, properties } |
archiveContact | Archive a contact by contactId | Empty (HTTP 204) |
searchContacts | Search with filterGroups/sorts/query, select properties | { total, results, paging } |
mergeContacts | Merge objectIdToMerge into primaryObjectId | { id, properties } |
batchReadContacts / batchCreateContacts / batchUpdateContacts / batchArchiveContacts | Batch ops on up to 100 inputs | { status, results } |
Companies
Same shape as contacts: listCompanies, getCompany, createCompany, updateCompany, archiveCompany, searchCompanies, mergeCompanies, batchReadCompanies, batchCreateCompanies, batchUpdateCompanies, batchArchiveCompanies.Deals
Same shape as contacts: listDeals, getDeal, createDeal, updateDeal, archiveDeal, searchDeals, mergeDeals, batchReadDeals, batchCreateDeals, batchUpdateDeals, batchArchiveDeals.Tickets
Same shape as the CRM trio, minus merge: listTickets, getTicket, createTicket, updateTicket, archiveTicket, searchTickets, batchReadTickets, batchCreateTickets, batchUpdateTickets, batchArchiveTickets.Associations
| Instruction | Description | Returns |
|---|
listAssociations | List associations from fromObjectType/fromObjectId to toObjectType | { results: [{ toObjectId, associationTypes }], paging } |
createAssociation | Default association between fromObject and toObject | { fromObjectTypeId, toObjectId, labels } |
archiveAssociation | Remove an association between two records | Empty (HTTP 204) |
Engagements (Notes / Tasks / Calls / Emails / Meetings)
| Instruction | Description | Returns |
|---|
listNotes / listTasks / listCalls / listEmails / listMeetings | List engagements; limit, after, properties, associations | { results, paging } |
getNote / getTask / getCall / getEmail / getMeeting | Get an engagement by its id | { id, properties, associations } |
createNote / createTask / createCall / createEmail / createMeeting | Create from properties, optional associations | { id, properties } |
updateNote / updateTask / updateCall / updateEmail / updateMeeting | Update an engagement by id | { id, properties } |
archiveNote / archiveTask / archiveCall / archiveEmail / archiveMeeting | Archive an engagement by id | Empty (HTTP 204) |
Pipelines
| Instruction | Description | Returns |
|---|
listPipelines | List pipelines for objectType (deals or tickets) | { results: [{ id, label, stages }] } |
getPipeline | Get a pipeline by objectType + pipelineId | { id, label, stages } |
createPipeline | Create a pipeline (label, stages, displayOrder) | { id, label, stages } |
updatePipeline | Update a pipeline by pipelineId | { id, label, stages } |
archivePipeline | Archive a pipeline by pipelineId | Empty (HTTP 204) |
listPipelineStages / getPipelineStage | List / get pipeline stages | { results } / { id, label, metadata } |
createPipelineStage / updatePipelineStage / archivePipelineStage | Manage a stage by stageId | { id, label, metadata } / 204 |
Properties
listProperties, getProperty, createProperty, updateProperty, archiveProperty, listPropertyGroups, createPropertyGroup, updatePropertyGroup, archivePropertyGroup — each scoped by objectType; returns property / property-group definitions.Owners
| Instruction | Description | Returns |
|---|
listOwners | List owners; optional email filter | { results: [{ id, email, firstName, lastName }], paging } |
getOwner | Get an owner by ownerId (match on idProperty) | { id, email, firstName, lastName } |
Lists
| Instruction | Description | Returns |
|---|
createList | Create a list (name, objectTypeId, processingType, filterBranch) | { list: { listId, name } } |
getList | Get a list by listId; includeFilters for the filter tree | { list: { listId, name, filtersUpdatedAt } } |
updateList | Rename a list (listId, listName) | { list } |
deleteList | Delete a list by listId | Empty (HTTP 204) |
searchLists | Search by query, paginate with count/offset | { lists, total, offset } |
addListMembers / removeListMembers | Add / remove members of a static list by listId | { recordsIdsAdded } / { recordsIdsRemoved } |
getListMemberships | List the members of a list by listId | { results, paging } |
| Instruction | Description | Returns |
|---|
listForms | List marketing forms; formTypes filter | { results, paging } |
getForm | Get a form by formId | { id, name, fieldGroups } |
createForm | Create a form (name, formType, fieldGroups, configuration) | { id, name } |
updateForm | Update a form by formId | { id, name } |
archiveForm | Archive a form by formId | Empty (HTTP 204) |
submitForm | Submit fields to a form (portalId, formGuid, context) | { inlineMessage } |
Marketing Emails
| Instruction | Description | Returns |
|---|
listMarketingEmails | List marketing emails; type, isPublished filters | { results, paging } |
getMarketingEmail | Get a marketing email by emailId; includeStats | { id, name, subject, stats } |
createMarketingEmail | Create a marketing email (name, subject, fromName, content) | { id, name } |
updateMarketingEmail | Update a marketing email by emailId | { id, name } |
archiveMarketingEmail | Archive a marketing email by emailId | Empty (HTTP 204) |
sendTransactionalEmail | Send a single-send email (emailId, message, contactProperties, customProperties) | { status, sendResult } |
Workflows
| Instruction | Description | Returns |
|---|
listWorkflows | List workflows; limit, offset | { workflows } |
getWorkflow | Get a workflow by workflowId | { id, name, enabled } |
enrollContactInWorkflow | Enroll a contact (workflowId, email) | Empty (HTTP 204) |
unenrollContactFromWorkflow | Unenroll a contact (workflowId, email) | Empty (HTTP 204) |
Files
| Instruction | Description | Returns |
|---|
listFiles | List files; filter by folder, name, type, extension | { results, paging } |
getFile | Get file metadata by fileId | { id, name, url, size } |
getFileSignedUrl | Temporary signed URL for a file (expirationSeconds, size, upscale) | { url, expiresAt } |
archiveFile | Archive a file by fileId | Empty (HTTP 204) |
importFileFromUrl | Async import from a public url (access, folderId, duplicateValidationStrategy) | { id, requestId } |
listFolders / createFolder / archiveFolder | Manage folders | { results } / { id, name } / 204 |
Returns shows the shape of the operation output (the underlying HubSpot API resource).
DSUL Examples
- Hubspot.searchContacts:
filterGroups:
- filters:
- propertyName: email
operator: EQ
value: '{{lead.email}}'
properties:
- email
- firstname
- lastname
- lifecyclestage
limit: 1
output: matched
Create a deal and associate it to a company
- Hubspot.createDeal:
properties:
dealname: '{{opportunity.name}}'
amount: '{{opportunity.amount}}'
dealstage: 'appointmentscheduled'
pipeline: 'default'
closedate: '{{opportunity.closeDate}}'
associations:
- to:
id: '{{companyId}}'
types:
- associationCategory: HUBSPOT_DEFINED
associationTypeId: 5
output: created
- Hubspot.addListMembers:
listId: '{{leadsListId}}'
recordIds:
- '{{contactId}}'
Send a transactional email
- Hubspot.sendTransactionalEmail:
emailId: '{{templateId}}'
message:
to: '{{lead.email}}'
from: 'noreply@acme.com'
contactProperties:
firstname: '{{lead.firstName}}'
customProperties:
orderId: '{{order.id}}'
orderTotal: '{{order.total}}'
- Hubspot.batchCreateContacts:
inputs:
- properties:
email: 'alice@acme.com'
firstname: 'Alice'
lastname: 'Doe'
- properties:
email: 'bob@acme.com'
firstname: 'Bob'
lastname: 'Smith'
output: batchResult