Events

Every cross-component interaction in Morpheus is an event. No function calls another function; nothing shares code at runtime. That makes the event catalog the platform’s real interface contract.

Transports

Transport Queue Routing key location Consumers

SQS FIFO

EnrichmentPipelineQueue.fifo

event_type message attribute

enrichment-scorer

SQS FIFO

CrmSyncQueue.fifo

event_type field in the body

morpheus-hubspot

SQS standard

EmailAgentQueue

event_type message attribute

email-agent

EventBridge

—

detail-type at the event root

all five functions

The routing key lives in a different place per queue. CrmSyncQueue consumers read event_type from the JSON body — which is also why a CrmSyncQueue message may be a JSON array of events. The other two queues read the event_type SQS message attribute, so their bodies need not carry it (though the models do).

Pages

Page Contents

Event catalog

Every event type in one table, with producer and consumer

CrmSyncQueue events

The 12 CRM event schemas

EnrichmentPipelineQueue events

SellerRecord.Received

EmailAgentQueue events

Email.IncomingCategorise, Resend.InboundEmail

Scheduled events

All 14 EventBridge detail-type values

HubSpot webhooks

The inbound webhook envelope and its processors

Publishing events

publish_event(), FIFO keys, deduplication

Adding a new event

The five-step walkthrough

Design rules

Fat events

An event carries everything its consumer needs, so the consumer performs no database read purely to do its job. Seller.CrmSync embeds the company fields and the full contact list, including each contact’s internal_id so the consumer can write ids back.

The exception is deliberate: Crm.DialerCallEnded is thin (call_id, target_id, campaign_id) because the Call row it points at is large, mutable, and authoritative. Embedding it would risk acting on stale data.

Pydantic models are the schema

Every event has a model in libs/morpheus-common/morpheus_common/events/models.py (cross-function) or in the function’s own models/ (function-local). The model is the contract — there is no separate schema registry. Cross-field requirements go in a model_validator(mode="after") so an invalid message is rejected at the boundary rather than mid-handler.

event_type defaults on the model
class ContactUpdateEvent(BaseModel):
    event_type: str = "Contact.Update"

+ A producer constructs the model and the routing key comes for free. It cannot drift from the registry key without someone editing the default.

Validate, then decide

The dispatcher validates. The handler decides. A handler never re-parses raw JSON.

Bad data is dropped, transient failure raises

A subscription mismatch, a missing row, or a malformed UUID logs at ERROR and returns — the message is deleted, because retrying cannot fix it. A Bedrock error or a HubSpot 5xx raises, so the message lands in the DLQ for inspection or replay.

Naming

Convention Pattern Examples

SQS event types

<Domain>.<Action>

Seller.CrmSync, Contact.Update, Crm.TaskCreate, FollowUpTask.Sync

Scheduled detail-types

<Function>.<Job>

Hubspot.EmailClickReEngagement, Phone.LineIntelPending, Scraper.BigQueryContactSync, Seller.EnrichPending

The scheduled prefix names the owning function, which makes it obvious from a CloudWatch log line which Lambda a schedule belongs to.

Reliability

All three queues use maxReceiveCount: 1 — no automatic retry. A single handler failure sends the message straight to its dead-letter queue, retained 14 days.

Queue DLQ

morpheus-sam-enrichment-pipeline.fifo

morpheus-sam-enrichment-pipeline-dlq.fifo

morpheus-sam-crm-sync.fifo

morpheus-sam-crm-sync-dlq.fifo

morpheus-sam-email-agent

morpheus-sam-email-agent-dlq

Every SQS trigger declares FunctionResponseTypes: [ReportBatchItemFailures], so a failure isolates to the offending message rather than the batch. BatchSize is 1 everywhere today.

Replay and inspection: Operations.

Idempotence

Consumers assume redelivery is possible and are written to tolerate it:

  • Database writes are ON CONFLICT DO UPDATE upserts on natural keys (sellerId, providerCallId, (subscriptionId, externalId)) or find-or-create.

  • Expensive steps are guarded by a completion marker: enrichedAt, payoutPainScore, syncTwilio, hubspotSyncStatus.

  • HubSpot’s "already has that value" response is treated as success, not failure.

  • FIFO deduplication ids are content-derived, so a duplicate publish inside the five-minute window is collapsed.

The known non-idempotent operation is HubSpot Call engagement creation in the Five9 sync — see Call logging.