EmailAgentQueue events
Queue: morpheus-sam-email-agent — a standard queue, not FIFO. Consumer:
email-agent.
Routing key: the event_type SQS message attribute. A record without it logs a warning
and is skipped.
Because the queue is standard, there are no message groups, no ordering guarantee, and no
MessageDeduplicationId. Producers use publish_to_standard_queue()
(lambdas/phone_agent/services/sqs_publisher.py) rather than
morpheus_common.publish_event(), which sets FIFO-only parameters.
Email.IncomingCategorise
Model: IncomingCategoriseEvent (lambdas/email_agent/models/incoming_categorise.py).
Producer: the Payability web application.
class IncomingCategoriseEvent(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
event_type: Literal["Email.IncomingCategorise"] = "Email.IncomingCategorise"
campaign_id: str # required — the campaign's UUID
contact_email: str | None # resolve target via (campaignId, email)
email_campaign_target_id: str | None # or resolve by primary key
inbound_subject: str = ""
inbound_text: str # required, min_length=1
variation_id: str | None # override; normally read from the target row
email_activity_id: str | None # correlation only
email_id: str | None # emails row to update alongside
@model_validator(mode="after")
def exactly_one_resolver(self): ...
|
|
Field notes:
| Field | Notes |
|---|---|
|
The campaign you already have in application context. Cross-checked against the resolved target’s |
|
Usually omitted. The handler reads the sent template from |
|
Id of an inbound email activity row already created elsewhere. For log correlation only — it is not used to resolve the target. |
|
Primary key of the |
Tenant scope |
Not in the payload. It comes from |
Examples
Resolve by email:
{
"event_type": "Email.IncomingCategorise",
"campaign_id": "REPLACE_CAMPAIGN_UUID",
"contact_email": "prospect@example.com",
"inbound_subject": "Re: Your note",
"email_id": "REPLACE_EMAIL_UUID",
"inbound_text": "Yes — can we do a call Tuesday afternoon?"
}
Resolve by target id:
{
"event_type": "Email.IncomingCategorise",
"campaign_id": "REPLACE_CAMPAIGN_UUID",
"email_campaign_target_id": "REPLACE_EMAIL_CAMPAIGN_TARGET_UUID",
"inbound_subject": "Re: Outreach",
"inbound_text": "Please remove me from your list.",
"email_id": "REPLACE_EMAIL_UUID",
"email_activity_id": null
}
Consumer behaviour and the seven categories: Email engagement.
Resend.InboundEmail
Model: ResendEmailReceivedEvent (lambdas/email_agent/models/events.py). Producer: a
Resend webhook relay.
class ResendAttachment(BaseModel):
model_config = ConfigDict(extra="ignore")
id: str
filename: str
content_type: str
content_disposition: str = ""
content_id: str = ""
class ResendEmailReceivedData(BaseModel):
model_config = ConfigDict(extra="ignore")
email_id: str
created_at: str
from_: str = Field(alias="from") # "from" is a Python keyword
to: list[str] = []
cc: list[str] = []
bcc: list[str] = []
message_id: str = ""
subject: str = ""
attachments: list[ResendAttachment] = []
class ResendEmailReceivedEvent(BaseModel):
model_config = ConfigDict(extra="ignore")
type: str
created_at: str
data: ResendEmailReceivedData
extra="ignore" throughout, so Resend can add fields without breaking validation.
The handler currently logs the email id, sender, subject, and attachment count, and does nothing else. Intended behaviour is listed in its module docstring — see email-agent.
Email.SendInternal
Published by phone-agent during stage-1 task escalation. No handler is registered.
{
"event_type": "Email.SendInternal",
"subscription_id": "b191a905-5d19-4fa0-941b-d8eb81c0c477",
"task_id": "…",
"sales_rep_email": "rep@example.com",
"sales_rep_name": "Example Rep",
"contact_id": "…",
"due_at": "2026-08-19T14:00:00+00:00",
"notes": "…"
}
The dispatcher logs "No handler registered for SQS event_type" and deletes the message. Stage-1 escalation therefore notifies by Slack DM only. To implement it: add a handler and model, register the key, and the existing producer works unchanged. See escalation ladder.
Local invocation
Sample files under lambdas/email_agent/events/samples/ are the inner SQS message
body, not the full Lambda event. Wrap one to invoke locally:
{
"Records": [
{
"messageId": "test-msg-1",
"eventSource": "aws:sqs",
"body": "{ …escaped JSON from a sample file… }",
"messageAttributes": {
"event_type": { "stringValue": "Email.IncomingCategorise", "dataType": "String" }
}
}
]
}
Replace every REPLACE_* placeholder before sending to a real queue.
Failure handling
maxReceiveCount: 1 — a handler failure sends the message to
morpheus-sam-email-agent-dlq, retained 14 days. Queue VisibilityTimeout is 180 s
against the consumer’s 120 s timeout.
| Failure | Fate |
|---|---|
Missing |
skipped, deleted |
Unregistered |
skipped, deleted |
Malformed JSON body |
DLQ |
|
DLQ |
Tenant mismatch, missing row, bad UUID |
deleted (the handler logs and returns) |
Bedrock error, or the model returning no structured output |
DLQ |