Lambda layers

Each library in libs/ is published as a Lambda layer by the SAM template. A function attaches only the layers it needs.

Logical id Layer name Source

MorpheusCommonLayer

morpheus-sam-common

libs/morpheus-common/

MorpheusLlmLayer

morpheus-sam-llm

libs/morpheus-llm/

MorpheusDataLayer

morpheus-sam-data-layer

libs/morpheus-data-layer/

MorpheusBigQueryLayer

morpheus-sam-bigquery

libs/morpheus-bigquery/

PybHubspotLayer

morpheus-sam-pyb-hubspot

libs/pyb-hubspot/

All five declare CompatibleRuntimes: [python3.12], CompatibleArchitectures: [x86_64], BuildMethod: python3.12, and RetentionPolicy: Retain.

RetentionPolicy: Retain keeps every published version alive after a stack update. Layer versions are immutable and a function pins one, so deleting old versions would break any function still referencing them — including during a rollback.

Attachment

Function Layers, in order

enrichment-scraper

common, data-layer, bigquery

enrichment-scorer

common, data-layer, llm, pyb-hubspot

morpheus-hubspot

common, data-layer, pyb-hubspot, bigquery

email-agent

common, data-layer, llm

phone-agent

common, data-layer, bigquery

MorpheusCommonLayer is always first. Layers are extracted onto the path in order, and morpheus_common provides configure_sentry(), which every handler calls at cold start before it loads secrets or imports anything else. Ordering it after a heavier layer risks an import failing before error reporting is available to record it.

The conventional order after that is data-layer, then llm, then the integration layers.

morpheus-common is on all five functions; morpheus-data-layer is too. morpheus-llm is attached only to the two functions that call Bedrock, morpheus-bigquery only to the three that query the warehouse, and pyb-hubspot only to the two that talk to the CRM.

Keeping the set minimal is not cosmetic: layers are extracted at cold start, and every megabyte of unused dependency is latency paid on every cold invocation. morpheus-llm pulls in the Strands SDK and its transitive dependencies, which is why phone-agent — which needs none of it — does not carry it.

How they are built

BuildMethod: python3.12 tells sam build to install each layer’s dependencies with pip into a python/ directory inside the layer archive, which is where the Lambda runtime looks.

The dependency list itself comes from the requirements.txt generated ahead of the build by lambdas/Makefile:

uv export --no-dev --no-hashes -o requirements.txt

make deploy runs that export for every function before sam build --no-cached && sam deploy.

Libraries are developed with uv and deployed with pip. The uv export step is the bridge: pyproject.toml and uv.lock are the source of truth, and requirements.txt is a build artefact regenerated on every deploy.

That is why a hand-edited requirements.txt silently disappears — the next make deploy overwrites it. Add the dependency with uv add in the library directory instead.

--no-cached forces a clean build. Layer builds cache aggressively, and a cached layer that predates a source change is a genuinely confusing failure: the deploy succeeds and the old code runs.

Importing from a layer

A layer’s package is a normal top-level import — the extraction path is already on sys.path:

from morpheus_common.sentry import configure as configure_sentry
from morpheus_data_layer.repositories import SellerRepository
from morpheus_data_layer.session import get_session, init
from morpheus_llm.providers import ClaudeAgentService
from morpheus_bigquery import create_bigquery_client
from pyb_hubspot import ...

Never import from agentcore.* or from another Lambda’s package. The deployment units are independent, and the only shared code is what is packaged as a layer.

Testing without a layer

Tests run on a developer machine where no layer is installed. The pattern that makes that work is a deferred import inside an @lru_cache function:

@lru_cache(maxsize=1)
def load_secrets() -> Secrets:
    import boto3                       (1)
    ...
1 Imported when first called, not at module load.

The module can then be imported by a test that mocks load_secrets() entirely, without the layer’s dependencies being present. The caching is doing double duty: one Secrets Manager call per container lifetime, and one import. See Testing Lambdas.

Adding a layer

  1. Create the library under libs/<name>/ with a pyproject.toml. Manage dependencies with uv add.

  2. Add an AWS::Serverless::LayerVersion to lambdas/template.yaml with ContentUri, CompatibleRuntimes: [python3.12], CompatibleArchitectures: [x86_64], BuildMethod: python3.12, and RetentionPolicy: Retain.

  3. Add a sync-<name> step to lambdas/Makefile so the export runs before the build.

  4. Reference it from each function’s Layers list, after MorpheusCommonLayer.

  5. Document it in Shared libraries and add a row to this page.

Constraints

Layers are versioned and immutable

sam deploy publishes a new version and repoints the functions. Rolling back republishes an earlier one.

Five layers is the hard limit

AWS allows a maximum of five layers per function. enrichment-scorer and morpheus-hubspot carry four. A sixth shared library cannot be attached to those functions without merging two.

250 MB unzipped, all layers plus code

The Lambda deployment size limit is shared across the function and every layer. morpheus-llm is the largest contributor.

Architecture must match

Layers declare x86_64, matching the SAM Globals. A function switched to arm64 will fail to load every one of them.