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 |
|
|
enrichment-scorer |
SQS FIFO |
|
|
morpheus-hubspot |
SQS standard |
|
|
email-agent |
EventBridge |
— |
|
all five functions |
|
The routing key lives in a different place per queue. |
Pages
| Page | Contents |
|---|---|
Every event type in one table, with producer and consumer |
|
The 12 CRM event schemas |
|
|
|
|
|
All 14 EventBridge |
|
The inbound webhook envelope and its processors |
|
|
|
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.CrmSyncembeds the company fields and the full contact list, including each contact’sinternal_idso the consumer can write ids back.The exception is deliberate:
Crm.DialerCallEndedis thin (call_id,target_id,campaign_id) because theCallrow 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 ownmodels/(function-local). The model is the contract — there is no separate schema registry. Cross-field requirements go in amodel_validator(mode="after")so an invalid message is rejected at the boundary rather than mid-handler. event_typedefaults 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
ERRORand 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 |
|
|
Scheduled detail-types |
|
|
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 |
|---|---|
|
|
|
|
|
|
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 UPDATEupserts 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.