Lambda functions

Five Python 3.12 functions, all defined in one SAM template (lambdas/template.yaml), all built from the same internal shape: a dispatcher, a registry, and one handler module per event type.

The five functions

Function Physical name Triggers Owns

enrichment-scraper

morpheus-sam-enrichment-scraper

EventBridge schedules

Seller discovery from SmartScout; BigQuery contact sync

enrichment-scorer

morpheus-sam-enrichment-scorer

EnrichmentPipelineQueue + schedules

Seller persistence, AI enrichment, payout-pain scoring

morpheus-hubspot

morpheus-sam-hubspot-sync

CrmSyncQueue + 9 schedules

Every HubSpot write; re-engagement segmentation; engagement and call sync

email-agent

morpheus-sam-email-agent

EmailAgentQueue

Inbound reply categorisation

phone-agent

morpheus-sam-phone-agent

3 schedules

Twilio line intelligence; Slack task reminders and escalation; campaign re-engagement

Resource configuration

Function Timeout Memory Reserved concurrency Layers

enrichment-scraper

300 s

256 MB

—

common, data, bigquery

enrichment-scorer

120 s

512 MB

5

common, data, llm, pyb-hubspot

morpheus-hubspot

300 s

256 MB

—

common, data, pyb-hubspot, bigquery

email-agent

120 s

512 MB

5

common, data, llm

phone-agent

120 s

256 MB

2

common, data, bigquery

All are x86_64 (Globals.Function.Architectures). Timeouts are chosen to sit below the source queue’s VisibilityTimeout where one applies — 120 s against 180 s for the enrichment and email queues, 300 s against 360 s for CrmSyncQueue.

Shape of a function

lambdas/<function>/
├── handler.py        lambda_handler — dispatcher only, no business logic
├── registry.py       routing key → (handler class, Pydantic model)
├── events/           one module per event type
│   ├── base.py       the abstract handler base class
│   └── samples/      example payloads for sam local invoke and tests
├── models/
│   ├── config.py     pydantic-settings Settings — environment variables
│   ├── secrets.py    load_secrets() — @lru_cache over the shared secret
│   └── events.py     event payload models local to this function
├── services/         integrations and orchestration used by handlers
├── utils/            constants, logging, pure helpers
├── pyproject.toml    dependencies (uv)
└── requirements.txt  exported deploy manifest

The pattern is uniform enough that reading one function teaches you all five. Start with Handler and registry pattern.

Cold start

Every handler.py runs the same three steps at module import, in this order:

configure_sentry()                                # 1. no-op when SENTRY_DSN is empty
_secrets = load_secrets()                         # 2. one Secrets Manager call, @lru_cache
init_db(_secrets.database_url, pool_size=0)       # 3. NullPool engine

pool_size=0 selects SQLAlchemy’s NullPool. Each Lambda container handles one invocation at a time and gains nothing from a persistent pool, while NullPool lets Supabase’s transaction-mode pooler multiplex efficiently across hundreds of containers. See pooling.

Cross-function communication

Only through queues. No function calls another, and no function imports another’s code.

flowchart LR
  Scraper["enrichment-scraper"] -->|SellerRecord.Received| Q1[["EnrichmentPipelineQueue"]]
  Q1 --> Scorer["enrichment-scorer"]
  Scorer -->|Seller.CrmSync| Q2[["CrmSyncQueue"]]
  Phone["phone-agent"] -->|Contact.Update| Q2
  Phone -->|FollowUpTask.Sync| Q2
  Phone -->|Email.SendInternal| Q3[["EmailAgentQueue"]]
  Q2 --> Hub["morpheus-hubspot"]
  Q3 --> Email["email-agent"]
  Ext(["web application / webhook relays"]) --> Q2
  Ext --> Q3

The full inventory is in Event catalog.

Where the logic is concentrated

morpheus-hubspot is by far the largest function: 12 SQS event types, 9 scheduled handlers, and the two heaviest modules in the repository (services/dialer_call_sync.py at 688 lines and events/scheduled/five9_call_sync.py at 675). It is the single writer to HubSpot, which is why so much converges there.

phone-agent is the second-densest, mostly because task escalation and campaign re-engagement are both non-trivial state machines.

Testing

cd lambdas && uv run pytest tests/ -v                     # cross-function
cd lambdas && uv run pytest morpheus_hubspot/tests -v     # hubspot handlers
cd lambdas/enrichment_scraper && uv run pytest tests -v   # SmartScout provider