Scheduled (EventBridge) events
Scheduled work is dispatched by exactly the same mechanism as queue work. The difference
is only where the routing key comes from: an EventBridge rule’s Input override supplies
a detail-type at the event root.
The Input pattern
Events:
EmailClickReEngagement:
Type: Schedule
Properties:
Name: !Sub "${AWS::StackName}-email-click-reengagement"
Description: "Re-engage contacts who clicked emails"
Schedule: rate(6 hours)
Enabled: true
Input: '{"detail-type":"Hubspot.EmailClickReEngagement","source":"morpheus.hubspot","detail":{}}'
Input replaces the whole event body, so the delivered event is exactly that JSON — none
of EventBridge’s usual envelope fields (version, id, account, region, time) are
present.
|
The |
ScheduledEvent
Each function defines its own, all identical:
class ScheduledEvent(BaseModel):
"""Generic EventBridge scheduled-rule event."""
model_config = ConfigDict(extra="ignore")
source: str = ""
extra="ignore" tolerates the whole envelope, and the only declared field is optional. A
scheduled event carries no payload, so there is nothing to share between functions and
nothing to validate — the model exists purely so the dispatcher’s model_validate() call
is uniform.
Two scraper events are richer, because they do carry parameters:
class SellerDiscoveryDetail(BaseModel):
model_config = ConfigDict(extra="allow")
marketplace: str = "amazon"
provider: str = "smartscout"
max_pages: int | None = None
class SellerDiscoveryEvent(BaseModel):
model_config = ConfigDict(populate_by_name=True)
detail_type: str = Field(alias="detail-type")
source: str
id: str | None = None # optional: an Input override omits the envelope
version: str | None = None
account: str | None = None
region: str | None = None
detail: SellerDiscoveryDetail = Field(default_factory=SellerDiscoveryDetail)
Every detail field defaults, so the rule’s Input need only specify what differs.
The catalogue
enrichment-scraper
detail-type |
Model | Purpose |
|---|---|---|
|
|
Paginated SmartScout discovery — rules |
|
|
Stub, raises |
|
|
Stub, ad-hoc per-category discovery |
|
|
Incremental BigQuery contact mirror |
enrichment-scorer
detail-type |
Model | Purpose |
|---|---|---|
|
|
Enrich up to 10 sellers with |
|
|
Score up to 10 enriched sellers with |
morpheus-hubspot
detail-type |
Model | Purpose |
|---|---|---|
|
|
Poll HubSpot for completed tasks — rules |
|
|
Wide-search HubSpot id resolution — rules |
|
|
Mirror engagements into |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dispatch and failure
The scheduled path differs from the SQS path in one important way: failures propagate.
def _handle_eventbridge(event, detail_type, context) -> None:
registration = event_registry.lookup(detail_type)
if registration is None:
logger.warning("No handler registered for detail-type %r. Registered types: %s", …)
return # silent no-op
try:
parsed = registration.event_model.model_validate(event)
except ValidationError as exc:
logger.error("Event validation failed … Raw event (truncated): %s", json.dumps(event)[:500])
raise # propagate
try:
registration.handler_class().handle(parsed, context)
except Exception:
logger.error("Unhandled exception in handler %s:\n%s", …, traceback.format_exc())
raise # propagate
There is no queue to absorb a failure, so raising is what makes it visible in the function’s error metric. The next tick is the retry.
Most scheduled handlers iterate per row with an inner try/except, so one bad row does
not abort the batch — the exception path is for setup failures, not row failures.
Common early returns
Scheduled handlers return early, with a warning and no work, when a precondition is missing:
| Condition | Handlers affected |
|---|---|
|
|
The BigQuery query raised |
all BigQuery-backed handlers — logged at |
The view returned no rows |
all segment handlers |
|
|
|
|
|
|
An early return is not an error. It logs at WARNING or INFO and the invocation
succeeds — which is why a handler that silently does nothing is best diagnosed by reading
its log line rather than its error metric.
Invoking a scheduled handler manually
aws lambda invoke \
--function-name morpheus-sam-hubspot-sync \
--payload '{"detail-type":"Hubspot.ContactResolve","source":"morpheus.hubspot","detail":{}}' \
--cli-binary-format raw-in-base64-out /dev/stdout
For SellerDiscovery.Amazon, add parameters in detail:
{"detail-type":"SellerDiscovery.Amazon","source":"enrichment.scraper",
"detail":{"marketplace":"amazon","provider":"smartscout","max_pages":5}}
|
A manual invocation is production traffic. The re-engagement handlers PATCH live HubSpot
contacts; the Five9 sync creates HubSpot call engagements. Prefer a narrow payload (for
example |
Cadences
Live schedule expressions, which rule is enabled, and which are commented out: EventBridge schedules.