SWIRLS_
Cookbook

Enrich Lead Data

Enrich, score, and route leads with external APIs and an LLM.

Capture a lead, enrich it with company and person data from Clearbit, score it with Claude, and route it to sales, nurture, or archive.

form new_lead {
  label: "New Lead"
  enabled: true
  schema: @json {
    {
      "type": "object",
      "required": ["email"],
      "properties": {
        "email": { "type": "string", "title": "Email" },
        "company": { "type": "string", "title": "Company" },
        "source": { "type": "string", "title": "Source", "enum": ["website", "event", "referral", "other"] },
        "notes": { "type": "string", "title": "Notes" }
      },
      "additionalProperties": false
    }
  }
}

graph enrich_and_score {
  label: "Enrich and Score Lead"
  description: "Enrich with external data, score with LLM, route by priority"

  persistence {
    enabled: true
    condition: @ts {
      return true
    }
  }

  root {
    type: code
    label: "Normalize"
    inputSchema: @json {
      {
        "type": "object",
        "required": ["email"],
        "properties": {
          "email": { "type": "string" },
          "company": { "type": "string" },
          "source": { "type": "string" },
          "notes": { "type": "string" }
        },
        "additionalProperties": false
      }
    }
    outputSchema: @json {
      {
        "type": "object",
        "required": ["email", "company", "source", "notes"],
        "properties": {
          "email": { "type": "string" },
          "company": { "type": "string" },
          "source": { "type": "string" },
          "notes": { "type": "string" }
        },
        "additionalProperties": false
      }
    }
    code: @ts {
      const { email, company, source, notes } = context.nodes.root.input
      return {
        email: email.toLowerCase().trim(),
        company: (company ?? "").trim(),
        source: source ?? "other",
        notes: (notes ?? "").trim(),
      }
    }
  }

  node enrich_company {
    type: http
    label: "Enrich company"
    method: "GET"
    url: @ts {
      const domain = context.nodes.root.output.email.split("@")[1]
      return `https://api.clearbit.com/v2/companies/find?domain=${domain}`
    }
    headers: {
      "Authorization": @ts { return `Bearer ${context.secrets.CLEARBIT_API_KEY}` }
    }
    schema: @json {
      {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "industry": { "type": "string" },
          "employeesRange": { "type": "string" },
          "annualRevenue": { "type": "number" }
        }
      }
    }
  }

  node enrich_person {
    type: http
    label: "Enrich person"
    method: "GET"
    url: @ts {
      return `https://api.clearbit.com/v2/people/find?email=${context.nodes.root.output.email}`
    }
    headers: {
      "Authorization": @ts { return `Bearer ${context.secrets.CLEARBIT_API_KEY}` }
    }
    schema: @json {
      {
        "type": "object",
        "properties": {
          "fullName": { "type": "string" },
          "title": { "type": "string" },
          "seniority": { "type": "string" }
        }
      }
    }
  }

  node score {
    type: ai
    label: "Score lead"
    kind: object
    model: "anthropic/claude-3.5-sonnet"
    temperature: 0.2
    prompt: @ts {
      const { email, company, source, notes } = context.nodes.root.output
      const companyData = JSON.stringify(context.nodes.enrich_company?.output ?? {})
      const personData = JSON.stringify(context.nodes.enrich_person?.output ?? {})
      return `Score this lead from 0-100 based on fit.

Email: ${email}
Company: ${company}
Source: ${source}
Notes: ${notes}
Company data: ${companyData}
Person data: ${personData}

Return JSON with:
- "score": integer 0-100
- "reasoning": one sentence
- "intent_summary": brief buying intent summary`
    }
    schema: @json {
      {
        "type": "object",
        "required": ["score", "reasoning", "intent_summary"],
        "properties": {
          "score": { "type": "number" },
          "reasoning": { "type": "string" },
          "intent_summary": { "type": "string" }
        }
      }
    }
  }

  node route {
    type: switch
    label: "Route by score"
    cases: ["high", "medium", "low"]
    router: @ts {
      const score = context.nodes.score.output.score ?? 0
      if (score > 70) return "high"
      if (score > 40) return "medium"
      return "low"
    }
  }

  node notify_sales {
    type: email
    label: "Notify sales"
    from: @ts { return "[email protected]" }
    to: @ts { return "[email protected]" }
    subject: @ts {
      return `High-priority lead: ${context.nodes.root.output.email} (score: ${context.nodes.score.output.score})`
    }
    text: @ts {
      const { email, company } = context.nodes.root.output
      const { score, reasoning, intent_summary } = context.nodes.score.output
      return `New high-priority lead:

Email: ${email}
Company: ${company}
Score: ${score}/100
Reasoning: ${reasoning}
Intent: ${intent_summary}`
    }
  }

  node add_to_nurture {
    type: http
    label: "Add to nurture"
    method: "POST"
    url: @ts { return "https://api.example.com/nurture/enroll" }
    headers: { "Authorization": @ts { return `Bearer ${context.secrets.CRM_API_KEY}` } }
    body: @ts {
      return JSON.stringify({
        email: context.nodes.root.output.email,
        score: context.nodes.score.output.score,
        campaign: "medium-intent"
      })
    }
  }

  node archive {
    type: code
    label: "Archive"
    outputSchema: @json {
      { "type": "object", "required": ["status"], "properties": { "status": { "type": "string" } }, "additionalProperties": false }
    }
    code: @ts {
      return { status: "archived" }
    }
  }

  flow {
    root -> enrich_company
    root -> enrich_person
    enrich_company -> score
    enrich_person -> score
    score -> route
    route -["high"]-> notify_sales
    route -["medium"]-> add_to_nurture
    route -["low"]-> archive
  }
}

trigger on_new_lead {
  form:new_lead -> enrich_and_score
  enabled: true
}

How it works

  • The form (or a webhook trigger) captures the lead and passes it to the root node, which normalizes the email and trims whitespace.
  • enrich_company and enrich_person run in parallel. Both HTTP nodes call Clearbit using the lead's email domain and address. The DAG executes them concurrently and waits for both before continuing.
  • The score AI node receives all four inputs: the normalized lead fields plus both enrichment payloads. It returns a score (0-100), one-sentence reasoning, and a buying intent summary.
  • The route switch node reads the score and branches: above 70 is high, above 40 is medium, and the rest is low.
  • High-priority leads trigger an email to the sales team with the score and intent summary.
  • Medium-priority leads are enrolled in a nurture campaign via a POST to your CRM API.
  • Low-priority leads are archived with a status record.
  • Persistence is enabled unconditionally, so every score and routing decision is stored for conversion analysis.

Run it

swirls deploy
swirls trigger on_new_lead --data '{"email": "[email protected]", "company": "Acme", "source": "website"}'

Customize

  • Swap Clearbit for Apollo, ZoomInfo, or another enrichment provider by updating the url and headers on the two HTTP nodes.
  • Add a review step on the high branch so a sales manager approves leads before the email fires.
  • Query the persistence stream to track scoring accuracy and refine score thresholds over time.
  • Add a schedule trigger to re-score medium leads monthly with fresh enrichment data.
  • Add a webhook trigger for CRM integrations so leads flow in automatically without the form.

On this page