morpheus-hubspot

Physical name: morpheus-sam-hubspot-sync. Source: lambdas/morpheus_hubspot/.

The largest and busiest function in the platform. It is the only component that writes to HubSpot, which concentrates OAuth, property mapping, rate-limit exposure, and re-engagement segmentation in one place.

Aspect Value

Handler

handler.lambda_handler

Timeout

300 s (CrmSyncQueue VisibilityTimeout is 360 s)

Memory

256 MB

Layers

common, data-layer, pyb-hubspot, bigquery

SQS concurrency

MaximumConcurrency: 10, BatchSize: 1

SQS events — event_registry

Routing key comes from the event_type field inside the message body, not from a message attribute. A body may also be a JSON array of event objects, each dispatched individually.

event_type Handler Purpose

Seller.CrmSync

SellerCrmSyncHandler

Upsert a Company + its Contacts, log Notes on updates, write ids back

Contact.Update

ContactUpdateHandler

Generic Contact PATCH by id; optional target_phone_numbers write-back

Contact.Unsubscribe

ContactUnsubscribeHandler

Communication Preferences (email subscription types)

Contact.ResolveHubSpotIds

ContactResolveHubSpotIdsHandler

Backfill hubspotContactId from CRM search

Crm.NewActivity

CrmNewActivityHandler

Create a logged Email object, plus an incoming-email follow-up Task and an AgentEmail event

Crm.UpdateActivity

CrmUpdateActivityHandler

PATCH an existing Email object

Crm.CustomEvent

CrmCustomEventHandler

Send a whitelisted behavioural event

Crm.TaskCreate

CrmTaskCreateHandler

Create a HubSpot Task and mirror it in follow_up_tasks

Crm.DealUpdate

CrmDealUpdateHandler

Set a Deal stage, by deal id or across a contact’s open deals

Crm.DialerCallEnded

CrmDialerCallEndedHandler

AI dialer call sync — the densest handler in the repository

Crm.HubSpotWebhook

CrmHubSpotWebhookHandler

Dispatch inner HubSpot webhook events to processors

FollowUpTask.Sync

FollowUpTaskSyncHandler

Sync a task’s status to HubSpot, or accept HubSpot’s completed state

Payload schemas: CrmSyncQueue events. Business rules: CRM sync.

An unknown event_type is logged at ERROR and the message discarded (the dispatcher returns True), since no retry could route it.

Scheduled events — scheduled_registry

detail-type Handler Purpose

Hubspot.FollowUpTaskPoll

FollowUpTaskHubSpotPollHandler

Poll HubSpot for completed tasks

Hubspot.ContactResolve

ContactHubSpotResolveHandler

Resolve missing hubspotContactId with a wide search

Hubspot.BigQueryEngagementSync

BigQueryEngagementSyncHandler

Mirror HubSpot engagements into Activity

Hubspot.EmailClickReEngagement

EmailClickReEngagementHandler

Email-click re-engagement segment

Hubspot.SupplierAccountReEngagement

SupplierAccountReEngagementHandler

Supplier account-created segment

Hubspot.UnattendedMeetingReEngagement

UnattendedMeetingReEngagementHandler

Meeting no-show segment

Hubspot.LostDealReEngagement

LostDealReEngagementHandler

Lost/stalled deal segment

Hubspot.SalesRepCallListRefresh

SalesRepCallListRefreshHandler

Rebuild each rep’s dynamic HubSpot call list

Hubspot.Five9CallSync

Five9CallSyncHandler

Import Five9 CSV call logs from Files.com

All nine rules are enabled. Cadences are in EventBridge schedules; several are set to development values rather than what their descriptions claim.

Services

Handlers stay thin; orchestration lives in services/.

Module Lines Responsibility

dialer_call_sync.py

688

The whole Crm.DialerCallEnded pipeline: skip rules, owner selection, contact resolution, engagement creation, follow-up task, Nooks enrolment

contact_service.py

121

ensure_local_contact_row() — guarantee a local contacts row, backfilling from the CRM and repairing target foreign keys

lifecycle_stage.py

101

maybe_promote_lifecycle_stage() — rank-guarded promotion to MQL, best-effort

deal_update.py

65

Resolve deal ids from a Crm.DealUpdate event and update each stage

Business detail: Call logging and dispositions.

Webhook processors

Crm.HubSpotWebhook carries a batch of inner HubSpot notifications. The handler opens one session and one CRM client for the whole batch and dispatches each inner event through WebhookProcessorRegistry. Processors are ordered: the first whose applies_to() returns True handles that event exclusively.

Processor Matches

NewsletterOptOutProcessor

contact.propertyChange where property_name is hs_email_optout_<id> and the id equals 44467216

Processor contract:

  • applies_to(inner) → bool — match tightly on subscription_type and property_name.

  • process(*, session, client, subscription_id, inner) — read from HubSpot with client, persist with session and the data-layer repositories.

  • Do not commit inside a processor — the handler’s get_session() block commits once per message.

  • Never log full payloads or raw PII.

  • An unsupported inner shape must log and return, not raise, so SQS does not retry the whole batch for an unknown notification type.

Registering: append an instance to the default tuple in WebhookProcessorRegistry.init, putting more specific matchers before generic ones. See HubSpot webhooks.

HubSpot client

utils/hubspot_client.py builds a HubSpotSyncClient from HubspotSecrets, preferring the Private App token and falling back to the OAuth triple. HubspotSecrets validates at load time that at least one complete auth mode exists and raises otherwise, so a misconfigured secret fails the cold start rather than the first API call.

Every HubSpot API call goes through pyb-hubspot. Handlers never import from hubspot.* directly.

Configuration

Setting Default Purpose

AGENTCORE_SECRET_ARN

(template)

Shared secret

SUBSCRIPTION_ID

(template)

Events with a different subscription_id are rejected

CONTACT_HUBSPOT_RESOLVE_BATCH_SIZE

25 in template (code 100)

Rows per Contact.ResolveHubSpotIds run

HUBSPOT_TASK_POLL_BATCH_SIZE

100

Tasks per poll run

BIGQUERY_ENGAGEMENT_SYNC_BATCH_SIZE

1000

Engagement rows per sync

FIVE9_CALL_SYNC_BATCH_SIZE

500

Five9 rows per sync

LOG_LEVEL

INFO

HubspotSecrets reads DATABASE_URL, HUBSPOT_PRIVATE_APP_TOKEN, the OAuth triple, HUBSPOT_ENVIRONMENT, the three Nooks keys, BIGQUERY_CREDENTIALS, and FILES_COM_API_KEY.

Non-environment constants live in utils/constants.py: the newsletter subscription type id (44467216), the AI fallback owner id (86817215), all four BigQuery re-engagement queries, watermark keys and defaults, the Five9 disposition→GUID map, the outreach_list_reason strings, and the call-list name template.

HubSpot portal setup

The function authenticates as a HubSpot Private App (or OAuth app). Permissions are granted in the portal, not by IAM, so a missing scope surfaces as a 403 at runtime rather than at deploy.

Minimum scopes:

Area Scopes

CRM objects

Companies and Contacts read/write — the core sync

Emails

crm.objects.emails — create and update logged emails

Behavioral events

Send custom event occurrences, for Crm.CustomEvent

Communication Preferences

subscriptions-definition-read, subscriptions-status-read, subscriptions-status-write — required by Contact.Unsubscribe

Each CustomEventKind also needs a matching custom behavioral event defined in the portal before POST /events/v3/send will accept it.

Email subscription type ids

Contact.Unsubscribe sends a hubspot_subscription_type_id per update. These ids are portal-specific. Snapshot for portal 9298797, default brand (businessUnitId: 0):

Id Name Notes

11599822

Marketing Information

Marketing offers and updates; the default subscription type

44467216

Newsletter

Weekly email newsletter

31174826

One to One

HubSpot internal default (isInternal: true)

44467216 is the only one hardcoded, as HUBSPOT_NEWSLETTER_SUBSCRIPTION_TYPE_ID in utils/constants.py. The webhook processor matches hs_email_optout_44467216 against it to detect a newsletter opt-out. It is a code constant rather than an environment variable, so changing portals means changing code.

Re-fetch the current list rather than trusting this table:

curl -H "Authorization: Bearer $HUBSPOT_PRIVATE_APP_TOKEN" \
  https://api.hubapi.com/communication-preferences/2026-03/definitions

results[].id values are what to send. Deleting and recreating a subscription type in HubSpot issues a new id, which silently breaks any producer still sending the old one — the API accepts the call and updates nothing. Confirm under Settings → Marketing → Email → Subscription types before wiring a new producer.

The Communication Preferences API is email-oriented. Non-email channels are not fully represented, which is why pyb_hubspot always sends channel: EMAIL. See pyb-hubspot.

IAM

  • SQSPollerPolicy on CrmSyncQueue

  • secretsmanager:GetSecretValue on AgentcoreSecret

No Bedrock access — this function runs no agents.

Tests

cd lambdas && uv run pytest morpheus_hubspot/tests -v

Covers the four re-engagement segments, dialer filters, Five9 sync, lifecycle promotion, ensure_local_contact_row, the newsletter opt-out processor, the sales-rep call-list refresh, and BigQuery engagement sync.

Operating notes

  • Everything routes through one function, so a HubSpot outage backs up CrmSyncQueue and then fills CrmSyncDlq (maxReceiveCount: 1).

  • Empty BIGQUERY_CREDENTIALS disables four scheduled handlers at once — the three re-engagement segments and the engagement sync — each logging a warning.

  • Missing FILES_COM_API_KEY disables the Five9 sync.

  • Watermarks for the engagement and Five9 syncs live in configs. Rewinding the Five9 watermark re-creates HubSpot call engagements — see watermarks.

  • The dispatcher’s "Handler failed" ERROR line includes the full message body, so this log group can contain PII after a failure.