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 |
|---|---|---|---|
|
EventBridge schedules |
Seller discovery from SmartScout; BigQuery contact sync |
|
|
|
Seller persistence, AI enrichment, payout-pain scoring |
|
|
|
Every HubSpot write; re-engagement segmentation; engagement and call sync |
|
|
|
Inbound reply categorisation |
|
|
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.
See Lambda layers and SAM template.
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
See Testing Lambdas.