Daily call lists
A contact appears on a rep’s list for a day when three things are true at once:
-
It is assigned to them —
hubspot_owner_idon the contact matches the rep. -
Today’s date is stamped on it —
call_list_dateequals today. This is what makes a list "today’s" and what makes it drop off tomorrow. -
It has a priority —
call_priority1–4, where 4 is most urgent. Reps work top-down.
Nobody adds contacts by hand. Automation watches for signals that a contact is worth calling and stamps those three properties.
|
This page describes the mechanism. For the rules deciding which signals qualify, see Re-engagement segments. |
The three ways a contact lands on a list
Written for the sales team; the same rules in engineering detail are in Re-engagement segments.
1. They engaged on a phone call (Amazon seller follow-up)
When the AI dialer reaches an Amazon seller, that contact is queued for a human follow-up. Urgency depends on how recently the AI spoke to them:
-
AI talked to them in the last 3 days → priority 4
-
AI talked to them longer ago, or we are not sure → priority 3
-
The last contact was not from the AI → priority 1
These are round-robined across the team, so they go to whichever rep is next in line.
2. They clicked a link in one of our emails
A contact who clicks a marketing email link and does not already have an open deal is added back to their existing owner’s list. Freshness drives priority:
-
Clicked today or yesterday → priority 4
-
Clicked in the last 2–6 days → priority 3
-
Clicked a week or more ago → priority 2
Because they already have an owner, these stay with that person.
3. They created a Payability account but stalled
A supplier who signed up — a deal in the "appointment scheduled" stage created recently — but has not moved forward gets re-queued. Priority is a score blending several things:
-
Never been called? → top priority
-
How the last call went — connected, or left a live message, scores higher than a dead end like a wrong number
-
How long ago the last call was — the longer it has been, the higher it nudges
-
How many times we have already called — more prior attempts pushes it down, so we do not over-dial the same person
The score is rounded to a 1–4 priority. These also stay with their existing owner.
Two further segments feed the same lists: contacts who booked a meeting and did not attend and have since gone quiet, and contacts on lost or stalled deals that were not hard-disqualified.
A few things worth knowing
-
Lists clear daily. A contact you do not reach today may reappear tomorrow if it still qualifies — or drop off if the situation changed, for example because they booked a deal.
-
We avoid double-dialing. A person called recently is held back for a couple of days before being re-added.
-
Open deals are excluded. Once a contact has an active deal, they stop appearing on re-engagement lists.
-
Priority is your running order. Start at 4 and work down. A 4 means something happened recently that makes a call timely.
Questions about why a specific contact showed up, or did not? The
outreach_list_reason property on the contact records which automation enrolled
them, and the segment rules are in
Re-engagement segments.
Suppression rules
Suppression always beats enrolment. A contact is held back when any of these apply:
| Rule | Where enforced | Applies to |
|---|---|---|
Has an open or existing deal |
BigQuery view |
Email click ( |
Ever had a call with a blocked disposition — wrong number, do not call, not qualified, not a seller |
BigQuery |
Email click, no-show, lost deal, supplier |
Closed-lost for a hard-disqualifying reason |
BigQuery view |
Lost deal |
Unsubscribed |
|
Email click |
Already called today (US Eastern, any owner) |
Python, in the handler |
Supplier, no-show, lost deal |
Called by a human owner in the last 2 days |
Python, in the handler |
Email click |
Any contact activity in the last 30 days |
BigQuery view |
No-show — this segment specifically wants cold contacts |
No resolvable HubSpot contact id |
Python (counted as |
All segments |
An AI-owned call never suppresses a human follow-up. That distinction — comparing
the engagement owner against the AI fallback owner id 86817215 — is what lets a
contact the AI just spoke to become a priority 4 human call rather than being
treated as "recently contacted".
List materialisation
Stamping properties is only half the job; the rep needs a list to open. That is
Hubspot.SalesRepCallListRefresh
(lambdas/morpheus_hubspot/events/scheduled/sales_rep_call_list_refresh.py).
For each active sales rep with a HubSpot owner id:
-
Delete the rep’s existing call-segment list, if
sales_reps.hubspotCallSegmentIdis set. -
Create a fresh dynamic HubSpot Contacts list filtered to
hubspot_owner_id = <rep>ANDcall_list_date = <today>. -
Write back the new list id to
sales_reps.hubspotCallSegmentId.
The list is named from CALL_LIST_NAME_TEMPLATE:
Outreach-SmartList-{rep_name}-{MM-DD-YYYY}. The HubSpot object type id for
Contacts is 0-1.
Each rep is processed best-effort — one rep’s failure is logged and does not stop the batch. Rep fields are snapshotted out of the session before the HubSpot calls, so a database connection is not held open across CRM latency.
|
Deleting and recreating the list means the list id changes daily. Anything
holding a saved list id — a Five9 or Nooks import, a saved view, a bookmark —
must read |
The rule is ScheduleV2 with ScheduleExpressionTimezone: America/New_York,
intended for 6 AM ET daily. The schedule expression is currently
rate(6 hours); the cron(0 6 * * ? *) line is commented out. See
EventBridge schedules.
"Today" in US Eastern
Handlers derive today’s date by adding a fixed -4 hour offset to UTC and taking
the date, rather than using a timezone database:
_US_EASTERN_UTC_OFFSET = timedelta(hours=-4)
today_et = (datetime.now(timezone.utc) + _US_EASTERN_UTC_OFFSET).strftime("%Y-%m-%d")
This is exact during EDT. During EST (UTC−5) it is off by one hour, so a handler running between 04:00 and 05:00 UTC can compute tomorrow’s date. The list refresh runs at 6 AM ET, an hour at which both offsets agree on the calendar date, so the materialised list is unaffected; the re-engagement handlers run throughout the day and are the ones exposed to the edge.
The BigQuery views do this properly, using
CURRENT_DATE('America/New_York') and DATE(ts, 'America/New_York').
Ordering within a priority
call_priority is the only ordering signal the rep sees. Within a priority band
the segments order their own candidate rows before the cap
(ORDER BY priority_score DESC, last_call_date ASC, and LIMIT 5000 or 2000), so
ordering only matters for deciding which rows get enrolled at all when a segment
exceeds its cap.
Diagnosing a missing contact
-
Read
outreach_list_reasonandcall_list_dateon the HubSpot contact. Stale date means no segment enrolled them today. -
Check whether they now have a deal — that excludes most segments.
-
Check for a blocked disposition on any of their calls.
-
Check whether they were called today, or in the last 2 days by a human.
-
Confirm the contact has a
hubspotContactIdin the local database — the email click and campaign segments resolve identity through it and drop rows without it. -
Confirm their owner is an active
sales_repsrow; a contact owned by an inactive rep is reassigned, so it may be on someone else’s list. -
Confirm the contact has a
phonevalue. The list filter requiresphone IS_KNOWN, and no segment checks it before enrolling — a contact with onlymobilephonegetscall_list_datewritten and still appears on no list. -
Check the handler’s completion line in CloudWatch:
… completed updated=N assigned=N skipped_no_hs_id=N total=N. -
If that completion line is absent, the run was killed by the Lambda timeout part-way through. Look for
Task timed outin the function’s log group. A truncated run always drops the same rows — whichever ones the segment’sORDER BYputs last — so a contact can be missing every day while the job reports no errors.