Lead groups
Leads live once in the CRM; lists point at them. How sourcing, review, and enrollment fit together.
Rilo follows the standard outbound stack shape: a lead is stored once in the CRM
(people / companies), lead groups are lists that point at leads, sequences run on
people, and exactly one flag can stop a running campaign.
SOURCE ──▶ LIST ──▶ REVIEW ──▶ ENROLL ──▶ RUNNING ──▶ OUTCOME
(search, (lead (mark_ (add_lead_ (drip (replied /
import, group) leads) group_to_ scheduler) completed /
attach) sequence) stopped)The three verdicts on a lead
A lead carries exactly three decision fields. Nothing else gates outreach.
| Field | Values | What it gates |
|---|---|---|
qualification_status | new → qualified / disqualified / customer | Entry into sequences: disqualified leaves every group's remaining pool. Never stops a running sequence. |
qualification_reason | free text, required on disqualify | Nothing - it is the audit trail. Cleared on every status transition. |
do_not_contact | true / false | Everything. true stops active enrollments, cancels planned sends, blocks one-off messages. The only kill switch. |
Rules worth internalizing:
- One verdict per lead, global. There is no per-list qualification - "not right for this list" is expressed by not enrolling that list.
- Qualification never touches a running sequence. Disqualify mid-campaign and the
sends continue; the response signals this and points at
do_not_contact/remove_from_sequenceif stopping is the intent. - Overrides are legal but loud.
disqualified→qualifiedis allowed; the response signals the flip with the previous status and reason.
Sourcing
| Path | Operation | Dedup key |
|---|---|---|
| Provider search | search_people, search_companies, search_companies_hiring, search_companies_similar | person: linkedin_url → email; company: domain |
| Bring your own rows | import_leads | same keys - a row without one is a validation error |
| Already in CRM | add_leads_to_group | n/a (by id) |
Re-sourcing an existing lead merges (fill-empty-only) and reports matched_existing,
which other groups already hold it, and whether it is mid-sequence (duplicates[]).
Lists are membership, not data
A lead group = immutable kind (companies or people) + recipe (steps + cursor) +
same-kind roster + log. One lead can be in many lists. Membership is append-only -
removal from a list does not exist. A lead leaves the working pool by verdict
(disqualified, with a reason), and a whole mistaken list is retired with
update_lead_group(archived=true) (reversible).
Lifecycle: born draft (unnamed search) or saved (named at creation or renamed later);
archived retires it from listings without touching members or lead data.
Company-first sourcing
Source and target are two distinct, linked groups - a company group is never converted into a people group:
search_companies(...)opens the company scaffold.mark_leads(..., company_ids=..., status="qualified")records which accounts may feed people discovery.search_people(within_lead_group_id=<company group>, per_company_limit=3, ...)opens a separate people target linked throughsource_lead_group_id.
Only qualified, non-DNC company members are searched. Creating the first linked people
target freezes company continuation: get_lead_group(source) returns has_more=false
and continuation_disabled=true, and get_more_leads
on the source returns source_group_frozen. A company-only group can never be enrolled
(invalid_lead_group_kind); enroll the linked people target.
Never pass the company group as lead_group_id to search_people - a different source,
persona, or per-company cap gets a new linked people target.
Review
mark_leads(lead_group_id, person_ids/company_ids, status, reason, do_not_contact?)
is bulk by design - one call for N members. The group is the targeting scope (ids must
be members); the write is global. Non-members land in skipped_not_member instead of
failing the batch.
Enrollment and remaining
add_lead_group_to_sequence enrolls a people
group's remaining pool:
remaining = members
− already enrolled/active in any sequence
− do_not_contact
− disqualifiedEvery skip at enroll time is itemized in skipped[] with a reason (where_mismatch,
do_not_contact, active_in_other_sequence, already_enrolled, previously_replied) -
never a silent drop. One cold sequence at a time per person; one enrollment row per
(person, sequence), enforced by the database.
Typical flows
People-first: list_lead_groups → get_lead_group → optional get_more_leads →
add_lead_group_to_sequence. Probe a new search with sample=true, commit with name=….
Company-first: search_companies → inspect → mark_leads(status="qualified") →
search_people(within_lead_group_id=…) → enroll the linked people group.
When a group's rows are thin, enrich them with
get_person_profile;
once a group is reviewed, the natural next step is enrollment.