← Back to cookbook

Lead Scoring

Scores incoming leads, persists results, and alerts sales on hot ones.

aiswitchresend

Source

/**
 * Scores incoming leads and persists results for reporting.
 * High-scoring leads trigger an alert to the sales team.
 */

webhook new_lead {
  label: "New Lead Webhook"
  schema: @json {
    {
      "type": "object",
      "required": ["email", "company", "source"],
      "properties": {
        "email": { "type": "string", "format": "email" },
        "company": { "type": "string" },
        "source": { "type": "string" },
        "job_title": { "type": "string" },
        "company_size": { "type": "string" }
      }
    }
  }
}

workflow score_lead {
  label: "Score Lead"

  root {
    type: code
    label: "Extract lead"
    code: @ts { return context.nodes.root.input }
    outputSchema: @json {
      {
        "type": "object",
        "properties": {
          "email": { "type": "string" },
          "company": { "type": "string" },
          "source": { "type": "string" },
          "job_title": { "type": "string" },
          "company_size": { "type": "string" }
        }
      }
    }
  }

  node score {
    type: ai
    label: "Score the lead"
    kind: object
    model: "google/gemini-2.5-flash"
    prompt: @ts {
      const lead = context.nodes.root.output
      return "Score this B2B lead from 0-100 based on likelihood to convert.\n\nEmail: " + lead.email + "\nCompany: " + lead.company + "\nSource: " + lead.source + "\nJob title: " + (lead.job_title || "unknown") + "\nCompany size: " + (lead.company_size || "unknown")
    }
    schema: @json {
      {
        "type": "object",
        "required": ["score", "reasoning"],
        "properties": {
          "score": { "type": "number", "minimum": 0, "maximum": 100 },
          "reasoning": { "type": "string" },
          "tier": { "type": "string", "enum": ["hot", "warm", "cold"] }
        }
      }
    }
  }

  node route {
    type: switch
    label: "Route by score"
    cases: ["hot", "other"]
    router: @ts {
      if (context.nodes.score.output.tier === "hot") return "hot"
      return "other"
    }
  }

  node alert_sales {
    type: email
    label: "Alert sales team"
    from: @ts { return "[email protected]" }
    to: @ts { return "[email protected]" }
    subject: @ts {
      return "Hot lead: " + context.nodes.root.output.company + " (Score: " + context.nodes.score.output.score + ")"
    }
    text: @ts {
      const lead = context.nodes.root.output
      const result = context.nodes.score.output
      return "New hot lead!\n\nCompany: " + lead.company + "\nEmail: " + lead.email + "\nScore: " + result.score + "\nReason: " + result.reasoning
    }
  }

  node log_result {
    type: code
    label: "Log score"
    code: @ts {
      return {
        email: context.nodes.root.output.email,
        company: context.nodes.root.output.company,
        score: context.nodes.score.output.score,
        tier: context.nodes.score.output.tier
      }
    }
  }

  flow {
    root -> score
    score -> route
    route -["hot"]-> alert_sales
    route -["other"]-> log_result
  }
}

stream scored_leads {
  label: "Scored leads"
  workflow: score_lead
  version: v1

  versions: {
    v1 {
      schema: @json {
        {
          "type": "object",
          "required": ["email", "company", "score", "tier"],
          "properties": {
            "email": { "type": "string" },
            "company": { "type": "string" },
            "score": { "type": "number" },
            "tier": { "type": "string" }
          }
        }
      }

      condition: @ts { return true }

      prepare: @ts {
        const logged = context.output.log_result
        if (logged) return logged
        return {
          email: context.nodes.root.output.email,
          company: context.nodes.root.output.company,
          score: context.nodes.score.output.score,
          tier: context.nodes.score.output.tier
        }
      }
    }
  }
}

trigger on_lead {
  webhook:new_lead -> score_lead
  enabled: true
}

Flow

Trigger to workflow

Workflow nodes