EventBridge schedules

Fourteen schedules drive the platform. Each is an EventBridge rule declared as an event on a function in lambdas/template.yaml, and each carries a static Input JSON document that becomes the entire event the handler receives.

Field Role

detail-type

The routing key. The function’s registry.py dispatches on this and nothing else.

source

Origin label, for readability and CloudWatch filtering.

detail

Usually {} — a scheduled handler derives its work from the database, not the payload.

Because the Input is static, a schedule is a trigger, never a parameter carrier. Anything a handler needs to know it looks up: the watermark from configs, the pending rows from PostgreSQL, the segment from BigQuery.

The detail-type string in template.yaml must match a registry.register(…​) key exactly. A typo produces no error at deploy time and no exception at runtime — the handler is simply never found, and the schedule fires into nothing. See Adding a new event.

morpheus-hubspot

Ten of the fourteen. Rule names are prefixed morpheus-sam-.

Rule Expression detail-type

hubspot-task-poll

rate(30 minutes)

Hubspot.FollowUpTaskPoll

hubspot-contact-resolve

rate(30 minutes)

Hubspot.ContactResolve

bigquery-engagement-sync

cron(0 9,20 * * ? *)

Hubspot.BigQueryEngagementSync

five9-call-sync

cron(0 9,20 * * ? *)

Hubspot.Five9CallSync

email-click-reengagement

rate(6 hours)

Hubspot.EmailClickReEngagement

supplier-account-reengagement

rate(6 hours)

Hubspot.SupplierAccountReEngagement

unattended-meeting-reengagement

rate(6 hours)

Hubspot.UnattendedMeetingReEngagement

lost-deal-reengagement

rate(6 hours)

Hubspot.LostDealReEngagement

sales-rep-call-list-refresh

rate(6 hours), timezone America/New_York

Hubspot.SalesRepCallListRefresh

The two cron(0 9,20 * * ? *) jobs run at 09:00 and 20:00 UTC — before and after the US business day, which is when the upstream exports have settled.

The four re-engagement schedules share a rate(6 hours) cadence and therefore fire together. That simultaneity is why the round-robin cursor is taken under an advisory lock: four handlers assigning reps at the same instant would otherwise all read the same cursor value. See Ownership and round-robin.

The one ScheduleV2

SalesRepCallListRefresh is the only rule declared as Type: ScheduleV2 rather than Type: Schedule, because it is the only one that needs ScheduleExpressionTimezone. It runs in America/New_York so the list rebuild tracks the sales team’s working day across daylight saving transitions, which a UTC cron cannot do.

The production expression cron(0 6 * * ? *) — a single daily rebuild at 06:00 Eastern — is present but commented out in favour of rate(6 hours). See Daily call lists.

phone-agent

Rule Expression detail-type

phone-line-intel-twice-daily

rate(3 hours)

Phone.LineIntelPending

task-deadline-reminder

cron(0 8,14,20 * * ? *)

Phone.TaskDeadlineReminder

campaign-reengagement

rate(6 hours)

Phone.CampaignTargetReEngagement

phone-line-intel-twice-daily is named for a cadence it no longer runs — the commented production expression is cron(0 6,18 * * ? *), but the deployed one is rate(3 hours), eight times a day. Each run processes up to PHONE_AGENT_BATCH_SIZE (25) numbers through Twilio Lookup, so the deployed rate spends roughly four times the intended quota. See Phone line intelligence.

task-deadline-reminder at 08:00, 14:00, and 20:00 UTC drives escalation: each run finds PENDING tasks past due with escalationStage < 3 and notifies Slack. Three runs a day against a cap of three stages means a stale task is escalated to exhaustion within 24 hours. See Follow-up tasks and escalation.

campaign-reengagement carries a commented production expression of cron(0 12 * * ? *).

enrichment-scraper

Rule Expression detail-type

bq-contact-sync

rate(2 hours)

Scraper.BigQueryContactSync

The scraper’s discovery schedules are commented out:

Rule Expression detail-type

AmazonDailyDiscovery

cron(0 7 * * ? *)

SellerDiscovery.Amazon

WalmartDailyDiscovery

cron(0 8 * * ? *)

also Enabled: false

The Amazon rule’s detail carries {"marketplace":"amazon","provider":"smartscout"} — one of the few schedules that does parameterise its handler.

With both discovery schedules disabled, no new sellers enter the funnel. The scraper’s only live job is mirroring HubSpot contacts from BigQuery into PostgreSQL. Together with the disabled scorer triggers, the entire discovery-and-enrichment half of the pipeline is dormant; the re-engagement and CRM half is not. See Outbound funnel.

enrichment-scorer

No active schedules. Both backfills are commented out:

Rule Expression detail-type

EnrichPendingBackfill

rate(60 hours)

Seller.EnrichPending

ScorePendingBackfill

rate(4 minutes)

Seller.ScorePending

rate(4 minutes) is a development value — at 10 sellers per run it would process 3,600 a day and hold a Bedrock call open most of the time. Re-enabling it as written would be expensive; the rate(60 hours) enrichment backfill is the more plausible production cadence of the two.

Reading the expressions

rate(n units) measures from the previous invocation’s completion, so a rate(6 hours) job that takes ten minutes runs every six hours and ten minutes and drifts. cron(…​) fires at fixed wall-clock times and does not drift.

The distinction matters where two jobs must not overlap. The four re-engagement handlers are all rate(6 hours) and drift together, which is fine because each is independently guarded; the two cron(0 9,20 * * ? *) syncs are pinned because they read a shared watermark window.

All cron(…​) expressions are UTC unless the rule sets ScheduleExpressionTimezone, which only SalesRepCallListRefresh does.

Changing a schedule

  1. Edit the expression in lambdas/template.yaml.

  2. If the detail-type changes, update the matching registry.register(…​) call — nothing validates the pair.

  3. make deploy-lambdas.

  4. Update this page and Scheduled events in the same pull request.

Disabling a schedule without a deploy is possible from the EventBridge console, but the change is invisible in the repository. For the scraper there is a better lever: the enrichment.<marketplace>.disabled key in the configs table, which the handler checks on every run. See Runtime state in configs.