Proactive and schedule-driven; each run is stateless — there is no local
ledger file. The one thing that spans runs is a pending bulk-update request:
its state lives in {{alert_channel}} itself (the posted draft plus any
approval reply), not in the sandbox.
Step 0 — Confirm access
No local state to resume — each run starts with a clean sandbox. The one
exception lives in Slack, not the sandbox: check {{alert_channel}} for an
unresolved "Pending bulk update" post from a previous run before assuming
this run's batches are new (see Step 5). Confirm the HubSpot and Clearbit
connectors are live before touching any records:
GET /crm/v3/objects/contacts?limit=1
GET /crm/v3/objects/deals?limit=1
If either connector fails, stop and report the failure in the summary
instead of running a partial sweep.
Step 1 — Pull the working set
Pull contacts and open deals touched or created in the last 24h, plus any
contact record with a null in a required field:
GET /crm/v3/objects/contacts/search
filterGroups: [{ propertyName: "lastmodifieddate", operator: "GTE", value: <24h ago> }]
GET /crm/v3/objects/contacts/search
filterGroups: [{ propertyName: "jobtitle", operator: "NOT_HAS_PROPERTY" }, ...]
GET /crm/v3/objects/deals/search
filterGroups: [{ propertyName: "dealstage", operator: "NEQ", value: "closedlost|closedwon" }]
Step 2 — Find and merge duplicate contacts
Two contacts are the same record when they match on any of:
- Same primary email address.
- Same first + last name AND same company domain.
A duplicate is often an older, untouched record that falls outside the
Step 1 working set (not modified in the last 24h, no missing field). For
each contact in the working set, run a targeted HubSpot search against the
full contact base — by primary email, and separately by
(firstname + lastname + company domain) — to surface any existing match
outside the 24h window, not just duplicates among contacts already pulled
in Step 1.
For each confirmed pair:
- Pick the primary: the record with the older
createdate, or the one with
more associated deals/tickets if createdate ties.
- Merge via HubSpot's contact merge endpoint so all associations (deals,
tickets, notes, timeline) roll onto the primary.
- If the two records disagree on a field HubSpot can't auto-resolve (e.g.
conflicting job titles), keep the primary's value and note the conflict
in the summary rather than guessing.
Step 3 — Fill missing fields from enrichment
Required fields: jobtitle, industry, numemployees (company size). For
each contact missing one or more:
- Query Clearbit by email or company domain.
- If Clearbit returns a value, write it to the missing field.
- Batch the writes for this pass. If the batch for a single field would
touch more than {{bulk_update_threshold}} records, do not write it —
go to Step 5 instead. Fills below the threshold write immediately,
per-record.
Step 4 — Flag stale deals
A deal is stale when it hasn't moved stage and has had no logged activity
(email, call, note, meeting) in 21+ days. For each match:
- Set a
hygiene_stale custom property (or tag) to true with the last
activity date.
- Never change
dealstage, never close the deal — flagging only.
Step 5 — Human approval gate for bulk updates
First, resolve any pending request from a previous run: search
{{alert_channel}} for the agent's own most recent "Pending bulk update:
" post.
- No pending post found, or the last one already carries this agent's own
"Applied"/"Resolved" reply → this field has nothing outstanding; if
Step 3 produced a new batch over {{bulk_update_threshold}} records for it,
go to "New batch" below.
- A pending post exists with no resolution reply yet → check whether an
authorized approver (not the agent) has replied "approve"/"approved" in
that thread:
- Approved — re-verify each record in the posted sample still needs
the fill (skip any that changed since the post), write the batch for
that field, reply in-thread confirming exactly what was applied, then
continue to Step 6. Do not also start a "New batch" for the same field
this run.
- No approval reply yet — do not write, and do not post a duplicate
request for the same field. Note it as still pending in this run's
summary (Step 6) and move on.
New batch — when Step 3 produces a batch over {{bulk_update_threshold}}
records for a field with no unresolved pending post:
- Do not write any record in that batch.
- Draft an approval request: field name, record count, and a sample of 5–10
affected records with old → new values.
- Post it to {{alert_channel}} as "Pending bulk update: " and stop —
do not write, do not wait inside this run. The batch is applied on a
later run, once that run's Step 5 finds an explicit approval reply in
this thread; never retry-write without one.
Step 6 — Post the summary
Post one summary to {{alert_channel}} covering:
- Duplicate contacts merged (count + any unresolved conflicts).
- Fields filled from enrichment (count per field).
- Deals flagged stale (count, with links).
- Any bulk update pending approval, what it is, and why it's waiting.