d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
392 lines
15 KiB
TypeScript
392 lines
15 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import { HubspotIcon } from '@/components/icons'
|
|
import { requestJson } from '@/lib/api/client/request'
|
|
import {
|
|
hubspotListsSelectorContract,
|
|
hubspotOwnersSelectorContract,
|
|
hubspotPipelinesSelectorContract,
|
|
hubspotPropertiesSelectorContract,
|
|
} from '@/lib/api/contracts/selectors/hubspot'
|
|
import { getScopesForService } from '@/lib/oauth/utils'
|
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
|
import type { TriggerConfig } from '@/triggers/types'
|
|
|
|
const logger = createLogger('HubSpotPollingTrigger')
|
|
|
|
/**
|
|
* Resolves the effective object type from the subblock store. `getValue` returns `null`
|
|
* for fields the user hasn't interacted with yet, so we fall back to the dropdown's
|
|
* default ('contact') — otherwise the cascading property selectors render empty on
|
|
* first render even when the dropdown visibly shows "contact".
|
|
*/
|
|
function resolveSelectedObjectType(blockId: string): string | null {
|
|
const objectType = useSubBlockStore.getState().getValue(blockId, 'objectType') as string | null
|
|
const customId = useSubBlockStore.getState().getValue(blockId, 'customObjectTypeId') as
|
|
| string
|
|
| null
|
|
const selected = objectType ?? 'contact'
|
|
if (selected === 'custom') {
|
|
const trimmed = customId?.trim()
|
|
return trimmed ? trimmed : null
|
|
}
|
|
return selected
|
|
}
|
|
|
|
async function fetchHubSpotProperties(blockId: string, objectType: string) {
|
|
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
|
|
| string
|
|
| null
|
|
if (!credentialId) throw new Error('No HubSpot credential selected')
|
|
const data = await requestJson(hubspotPropertiesSelectorContract, {
|
|
query: { credentialId, objectType },
|
|
})
|
|
return data.properties.map((p) => ({ id: p.id, label: p.name }))
|
|
}
|
|
|
|
export const hubspotPollingTrigger: TriggerConfig = {
|
|
id: 'hubspot_poller',
|
|
name: 'HubSpot CRM Trigger',
|
|
provider: 'hubspot',
|
|
description:
|
|
'Triggers when HubSpot CRM records (contacts, companies, deals, tickets, custom objects) are created or updated, or when contacts join a list',
|
|
version: '1.0.0',
|
|
icon: HubspotIcon,
|
|
polling: true,
|
|
|
|
subBlocks: [
|
|
{
|
|
id: 'triggerCredentials',
|
|
title: 'HubSpot Account',
|
|
type: 'oauth-input',
|
|
description: 'Connect a HubSpot account so Sim can poll your CRM on your behalf.',
|
|
serviceId: 'hubspot',
|
|
requiredScopes: getScopesForService('hubspot'),
|
|
required: true,
|
|
mode: 'trigger',
|
|
},
|
|
{
|
|
id: 'objectType',
|
|
title: 'Object Type',
|
|
type: 'dropdown',
|
|
description: 'What you want to watch.',
|
|
options: [
|
|
{ label: 'Contact', id: 'contact' },
|
|
{ label: 'Company', id: 'company' },
|
|
{ label: 'Deal', id: 'deal' },
|
|
{ label: 'Ticket', id: 'ticket' },
|
|
{ label: 'Custom Object', id: 'custom' },
|
|
{ label: 'List Membership', id: 'list_membership' },
|
|
],
|
|
defaultValue: 'contact',
|
|
required: true,
|
|
mode: 'trigger',
|
|
},
|
|
{
|
|
id: 'customObjectTypeId',
|
|
title: 'Custom Object Type ID',
|
|
type: 'short-input',
|
|
description:
|
|
'HubSpot custom object type ID (e.g. "2-12345"). Find it in HubSpot Settings → Objects → Custom Objects.',
|
|
placeholder: '2-12345',
|
|
required: { field: 'objectType', value: 'custom' },
|
|
mode: 'trigger',
|
|
condition: { field: 'objectType', value: 'custom' },
|
|
},
|
|
{
|
|
id: 'listId',
|
|
title: 'List',
|
|
type: 'dropdown',
|
|
description: 'The HubSpot list to watch for new members.',
|
|
placeholder: 'Select a list',
|
|
options: [],
|
|
fetchOptions: async (blockId: string) => {
|
|
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
|
|
| string
|
|
| null
|
|
if (!credentialId) throw new Error('No HubSpot credential selected')
|
|
try {
|
|
const data = await requestJson(hubspotListsSelectorContract, {
|
|
query: { credentialId },
|
|
})
|
|
return data.lists.map((l) => ({ id: l.id, label: l.name }))
|
|
} catch (error) {
|
|
logger.error('Error fetching HubSpot lists:', error)
|
|
throw error
|
|
}
|
|
},
|
|
dependsOn: ['triggerCredentials'],
|
|
required: { field: 'objectType', value: 'list_membership' },
|
|
mode: 'trigger',
|
|
condition: { field: 'objectType', value: 'list_membership' },
|
|
},
|
|
{
|
|
id: 'eventType',
|
|
title: 'Event',
|
|
type: 'dropdown',
|
|
description:
|
|
'Created fires once per new record. Updated fires on any modification. Property Changed fires only when the chosen property changes value.',
|
|
options: [
|
|
{ label: 'Created', id: 'created' },
|
|
{ label: 'Updated (any change)', id: 'updated' },
|
|
{ label: 'Property Changed', id: 'property_changed' },
|
|
],
|
|
defaultValue: 'created',
|
|
required: { field: 'objectType', value: 'list_membership', not: true },
|
|
mode: 'trigger',
|
|
condition: { field: 'objectType', value: 'list_membership', not: true },
|
|
},
|
|
{
|
|
id: 'targetPropertyName',
|
|
title: 'Property to Watch',
|
|
type: 'dropdown',
|
|
description: 'Fires only when this specific property changes value on a record.',
|
|
placeholder: 'Select a property',
|
|
options: [],
|
|
fetchOptions: async (blockId: string) => {
|
|
const resolved = resolveSelectedObjectType(blockId)
|
|
if (!resolved) throw new Error('Select an object type first')
|
|
try {
|
|
return await fetchHubSpotProperties(blockId, resolved)
|
|
} catch (error) {
|
|
logger.error('Error fetching HubSpot properties:', error)
|
|
throw error
|
|
}
|
|
},
|
|
dependsOn: ['triggerCredentials', 'objectType', 'customObjectTypeId'],
|
|
required: { field: 'eventType', value: 'property_changed' },
|
|
mode: 'trigger',
|
|
condition: {
|
|
field: 'eventType',
|
|
value: 'property_changed',
|
|
and: { field: 'objectType', value: 'list_membership', not: true },
|
|
},
|
|
},
|
|
{
|
|
id: 'properties',
|
|
title: 'Properties to Fetch',
|
|
type: 'dropdown',
|
|
multiSelect: true,
|
|
description:
|
|
'Properties to include on each record. Leave empty to use sensible defaults. Sim always includes the timestamps it needs internally.',
|
|
placeholder: 'Select properties (optional)',
|
|
options: [],
|
|
fetchOptions: async (blockId: string) => {
|
|
const resolved = resolveSelectedObjectType(blockId)
|
|
if (!resolved) return []
|
|
try {
|
|
return await fetchHubSpotProperties(blockId, resolved)
|
|
} catch (error) {
|
|
logger.error('Error fetching HubSpot properties:', error)
|
|
throw error
|
|
}
|
|
},
|
|
dependsOn: ['triggerCredentials', 'objectType', 'customObjectTypeId'],
|
|
required: false,
|
|
mode: 'trigger',
|
|
condition: { field: 'objectType', value: 'list_membership', not: true },
|
|
},
|
|
{
|
|
id: 'pipelineId',
|
|
title: 'Pipeline (optional)',
|
|
type: 'dropdown',
|
|
description: 'Restrict to a single pipeline.',
|
|
placeholder: 'All pipelines',
|
|
options: [],
|
|
fetchOptions: async (blockId: string) => {
|
|
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
|
|
| string
|
|
| null
|
|
const objectType = resolveSelectedObjectType(blockId) ?? 'contact'
|
|
if (!credentialId) throw new Error('No HubSpot credential selected')
|
|
try {
|
|
const data = await requestJson(hubspotPipelinesSelectorContract, {
|
|
query: { credentialId, objectType },
|
|
})
|
|
return data.pipelines.map((p) => ({ id: p.id, label: p.name }))
|
|
} catch (error) {
|
|
logger.error('Error fetching HubSpot pipelines:', error)
|
|
throw error
|
|
}
|
|
},
|
|
dependsOn: ['triggerCredentials', 'objectType'],
|
|
required: false,
|
|
mode: 'trigger',
|
|
condition: { field: 'objectType', value: ['deal', 'ticket'] },
|
|
},
|
|
{
|
|
id: 'stageId',
|
|
title: 'Stage (optional)',
|
|
type: 'dropdown',
|
|
description: 'Restrict to a single stage within the selected pipeline.',
|
|
placeholder: 'All stages',
|
|
options: [],
|
|
fetchOptions: async (blockId: string) => {
|
|
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
|
|
| string
|
|
| null
|
|
const objectType = resolveSelectedObjectType(blockId) ?? 'contact'
|
|
const pipelineId = useSubBlockStore.getState().getValue(blockId, 'pipelineId') as
|
|
| string
|
|
| null
|
|
if (!credentialId) throw new Error('No HubSpot credential selected')
|
|
if (!pipelineId) return []
|
|
try {
|
|
const data = await requestJson(hubspotPipelinesSelectorContract, {
|
|
query: { credentialId, objectType },
|
|
})
|
|
const pipeline = data.pipelines.find((p) => p.id === pipelineId)
|
|
return (pipeline?.stages ?? []).map((s) => ({ id: s.id, label: s.label }))
|
|
} catch (error) {
|
|
logger.error('Error fetching HubSpot stages:', error)
|
|
throw error
|
|
}
|
|
},
|
|
dependsOn: ['triggerCredentials', 'objectType', 'pipelineId'],
|
|
required: false,
|
|
mode: 'trigger',
|
|
condition: { field: 'pipelineId', value: '', not: true },
|
|
},
|
|
{
|
|
id: 'ownerId',
|
|
title: 'Owner (optional)',
|
|
type: 'dropdown',
|
|
description: 'Restrict to records owned by a specific HubSpot user.',
|
|
placeholder: 'Any owner',
|
|
options: [],
|
|
fetchOptions: async (blockId: string) => {
|
|
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
|
|
| string
|
|
| null
|
|
if (!credentialId) throw new Error('No HubSpot credential selected')
|
|
try {
|
|
const data = await requestJson(hubspotOwnersSelectorContract, {
|
|
query: { credentialId },
|
|
})
|
|
return data.owners.map((o) => ({ id: o.id, label: o.name }))
|
|
} catch (error) {
|
|
logger.error('Error fetching HubSpot owners:', error)
|
|
throw error
|
|
}
|
|
},
|
|
dependsOn: ['triggerCredentials'],
|
|
required: false,
|
|
mode: 'trigger',
|
|
condition: { field: 'objectType', value: 'list_membership', not: true },
|
|
},
|
|
{
|
|
id: 'filters',
|
|
title: 'Advanced Filters (optional)',
|
|
type: 'long-input',
|
|
description:
|
|
'JSON array of HubSpot search filters, AND-combined. Each item: {"propertyName":"...","operator":"EQ","value":"..."}. Operators: EQ, NEQ, CONTAINS_TOKEN, NOT_CONTAINS_TOKEN, GT, GTE, LT, LTE, BETWEEN, IN, NOT_IN, HAS_PROPERTY, NOT_HAS_PROPERTY.',
|
|
placeholder:
|
|
'[{"propertyName":"lifecyclestage","operator":"EQ","value":"customer"},{"propertyName":"amount","operator":"GT","value":"10000"}]',
|
|
required: false,
|
|
mode: 'trigger',
|
|
condition: { field: 'objectType', value: 'list_membership', not: true },
|
|
wandConfig: {
|
|
enabled: true,
|
|
maintainHistory: true,
|
|
prompt: `You are an expert HubSpot CRM developer. Generate a JSON array of HubSpot search filters based on the user's request.
|
|
|
|
Each filter is { propertyName, operator, value }. Filters are AND-combined. The available operators are: EQ, NEQ, CONTAINS_TOKEN, NOT_CONTAINS_TOKEN, GT, GTE, LT, LTE, BETWEEN, IN, NOT_IN, HAS_PROPERTY, NOT_HAS_PROPERTY.
|
|
|
|
Dates use millisecond epoch values as strings. Strings are exact (case-sensitive) for EQ/NEQ.
|
|
|
|
User context:
|
|
{context}
|
|
|
|
Return ONLY the JSON array — no explanations, no markdown, no code fences.`,
|
|
placeholder: 'Describe the records you want…',
|
|
generationType: 'json-object',
|
|
},
|
|
},
|
|
{
|
|
id: 'maxRecordsPerPoll',
|
|
title: 'Max Records Per Poll',
|
|
type: 'short-input',
|
|
description:
|
|
'Soft cap on records emitted per poll (default 50, max 1000). Excess rolls over to the next poll.',
|
|
placeholder: '50',
|
|
required: false,
|
|
mode: 'trigger',
|
|
},
|
|
{
|
|
id: 'triggerInstructions',
|
|
title: 'How it works',
|
|
hideFromPreview: true,
|
|
type: 'text',
|
|
defaultValue: [
|
|
'Connect your HubSpot account above.',
|
|
'Pick the object type and event you want to watch.',
|
|
'(Optional) Restrict by pipeline, stage, owner, or advanced filters.',
|
|
'Sim polls HubSpot every minute and fires this workflow for each new or updated record.',
|
|
'The first poll establishes a baseline — only records changed <em>after</em> activation will fire the workflow.',
|
|
]
|
|
.map(
|
|
(instruction, index) =>
|
|
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
|
|
)
|
|
.join(''),
|
|
mode: 'trigger',
|
|
},
|
|
],
|
|
|
|
outputs: {
|
|
objectType: {
|
|
type: 'string',
|
|
description:
|
|
'HubSpot object type (contact, company, deal, ticket, custom object id, or list_membership)',
|
|
},
|
|
eventType: {
|
|
type: 'string',
|
|
description: 'Event type (created, updated, property_changed, or joined)',
|
|
},
|
|
objectId: {
|
|
type: 'string',
|
|
description: 'HubSpot ID of the affected record (or contact id for list memberships)',
|
|
},
|
|
occurredAt: {
|
|
type: 'string',
|
|
description: 'ISO timestamp of when the change happened in HubSpot',
|
|
},
|
|
properties: {
|
|
type: 'json',
|
|
description:
|
|
'HubSpot properties on the record as a key-value object (property internal name → value). Default keys per object type (override via "Properties to Fetch"): Contact → firstname, lastname, email, phone, company, lifecyclestage, hs_lead_status, hubspot_owner_id, createdate, lastmodifieddate. Company → name, domain, industry, lifecyclestage, hubspot_owner_id, createdate, hs_lastmodifieddate. Deal → dealname, amount, dealstage, pipeline, closedate, hubspot_owner_id, createdate, hs_lastmodifieddate. Ticket → subject, content, hs_pipeline, hs_pipeline_stage, hs_ticket_priority, hubspot_owner_id, createdate, hs_lastmodifieddate. Custom and user-requested properties appear keyed by their HubSpot internal name.',
|
|
},
|
|
createdAt: {
|
|
type: 'string',
|
|
description: 'ISO timestamp when the record was created in HubSpot',
|
|
},
|
|
updatedAt: {
|
|
type: 'string',
|
|
description: 'ISO timestamp when the record was last updated in HubSpot',
|
|
},
|
|
archived: {
|
|
type: 'boolean',
|
|
description: 'Whether the record is archived',
|
|
},
|
|
propertyName: {
|
|
type: 'string',
|
|
description: 'Name of the property that changed (property_changed events only)',
|
|
},
|
|
propertyValue: {
|
|
type: 'string',
|
|
description: 'New value of the changed property (property_changed events only)',
|
|
},
|
|
previousValue: {
|
|
type: 'string',
|
|
description: 'Previous value before the change (property_changed events only)',
|
|
},
|
|
listId: {
|
|
type: 'string',
|
|
description: 'HubSpot list ID (list_membership events only)',
|
|
},
|
|
timestamp: {
|
|
type: 'string',
|
|
description: 'ISO timestamp when Sim emitted the event',
|
|
},
|
|
},
|
|
}
|