crm.leads.list & crm.leads.search #

Query leads across your module. Use crm.leads.list for structured multi-field filtering by stage, assignment, or status, and crm.leads.search for keyword matching across lead names, emails, and custom metadata attributes.


TypeScript Signatures #

typescript
// Structured multi-field filtering with pagination
crm.leads.list(filters?: {
  page?: number;
  limit?: number;
  status?: Array<"new" | "contacted" | "qualified" | "unqualified" | "converted">;
  query?: string;
  assignedTo?: number;
  stageId?: number;
}): Promise<PaginatedResult<Lead>>;

// Full-text keyword search across lead fields
crm.leads.search(
  query: string,
  options?: { page?: number; limit?: number }
): Promise<PaginatedResult<Lead>>;

Parameters Reference #

list(filters) #

ParameterTypeRequiredDefaultDescription
filters.pagenumberOptional1Page number index (1-based).
filters.limitnumberOptional50Maximum results per page (capped at 100).
filters.statusstring[]Optional—Array of lead status strings to match against.
filters.assignedTonumberOptional—Filter by the assigned user ID.
filters.stageIdnumberOptional—Filter by current pipeline stage ID.
filters.querystringOptional—Substring match against name, email, or phone.

search(query, options) #

ParameterTypeRequiredDefaultDescription
querystringYes—Search string to match across all searchable fields.
options.pagenumberOptional1Page number index.
options.limitnumberOptional20Results per page (capped at 100).

Returns: Promise<PaginatedResult<Lead>>

typescript
interface PaginatedResult<T> {
  data: T[];
  total: number;
}

Minimal Quickstart Example #

typescript
async function run(context: Context, dolfin: DolfinApi): Promise<void> {
  const { lead } = context;
  const crm = await dolfin.crm.module(lead.moduleId);

  // Search for other leads from the same company domain
  const domain = lead.email?.split("@")[1];
  if (domain) {
    const results = await crm.leads.search(domain, { limit: 10 });
    console.log(`Found ${results.total} matching leads with domain @${domain}`);
  }
}

Production Recipe: Inbound Duplicate Detection & Alerting #

This recipe runs on newly created leads, executes a full-text search against the incoming email address, detects existing records, and tags the lead while appending warning notes for the sales team.

typescript
async function run(context: Context, dolfin: DolfinApi): Promise<void> {
  const { lead } = context;
  const crm = await dolfin.crm.module(lead.moduleId);

  if (!lead.email) {
    console.log("No email provided; skipping duplicate detection.");
    return;
  }

  console.log(`Checking duplicates for lead #${lead.id} (${lead.email})`);

  try {
    // 1. Search existing leads with the same email
    const searchResults = await crm.leads.search(lead.email, { limit: 10 });

    // 2. Filter out current lead record
    const existingDuplicates = searchResults.data.filter((match) => match.id !== lead.id);

    if (existingDuplicates.length > 0) {
      console.warn(`Detected ${existingDuplicates.length} potential duplicate records.`);

      const duplicateSummary = existingDuplicates
        .map((d) => `#${d.id} ("${d.name}" in Stage ID ${d.stageId})`)
        .join(", ");

      // 3. Mark duplicate flag in custom metadata
      await crm.leads.update(lead.id, {
        meta: {
          ...lead.meta,
          has_possible_duplicate: true,
          duplicate_lead_ids: existingDuplicates.map((d) => d.id),
          detected_at: new Date().toISOString()
        }
      });

      // 4. Record warning timeline note
      await crm.leads.notes.add(
        lead.id,
        `⚠️ Duplicate Alert: Found ${existingDuplicates.length} existing lead(s) with matching email: ${duplicateSummary}.`
      );

      console.log(`Successfully tagged duplicate warnings on lead #${lead.id}.`);
    } else {
      console.log("No duplicates found. Lead is unique.");
    }
  } catch (error: any) {
    console.error(`Duplicate detection query failed: ${error.message}`);
    throw error;
  }
}

Common Errors & Troubleshooting #

Error MessageCauseResolution
Search query is requiredPassed an empty string "" or non-string query to search().Provide a non-empty keyword string.
Exceeded maximum CRM calls per execution (50)Iterated through pages or records issuing queries in an unmetered loop.Request larger page sizes (up to limit: 100) rather than querying 1 item per call.