Concepts

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.

FieldValuesWhat it gates
qualification_statusnewqualified / disqualified / customerEntry into sequences: disqualified leaves every group's remaining pool. Never stops a running sequence.
qualification_reasonfree text, required on disqualifyNothing - it is the audit trail. Cleared on every status transition.
do_not_contacttrue / falseEverything. 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_sequence if stopping is the intent.
  • Overrides are legal but loud. disqualifiedqualified is allowed; the response signals the flip with the previous status and reason.

Sourcing

PathOperationDedup key
Provider searchsearch_people, search_companies, search_companies_hiring, search_companies_similarperson: linkedin_urlemail; company: domain
Bring your own rowsimport_leadssame keys - a row without one is a validation error
Already in CRMadd_leads_to_groupn/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:

  1. search_companies(...) opens the company scaffold.
  2. mark_leads(..., company_ids=..., status="qualified") records which accounts may feed people discovery.
  3. search_people(within_lead_group_id=<company group>, per_company_limit=3, ...) opens a separate people target linked through source_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
          − disqualified

Every 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_groupsget_lead_group → optional get_more_leadsadd_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.

On this page