Recipe: lead enrichment & routing

Enrich an inbound lead over HTTP, score its fit with an LLM, and route it hot/warm/cold.

What it does: a new lead comes in via the API; an HTTP connector enriches it, an LLM scores its fit 0–100, a branch routes hot / nurture / disqualified, and the owner is emailed.

When to use it: inbound lead routing, support-ticket triage, any “enrich → score → route” classification pipeline that runs unattended.

Flow

receive ─▶ [connector: enrich (HTTP)] ─▶ [connector: score (LLM)] ─▶ route
              │ onError → enrich_failed (continue)                     ├─ score ≥ 80 ─▶ route_hot ─▶ notify ─▶ done
                                                                       ├─ score ≥ 50 ─▶ route_nurture ─▶ notify ─▶ done
                                                                       └─ else ───────▶ route_disqualify ─▶ done

The workflow

// Lead enrichment & routing — enrich a new lead, score it, route it.
//
// A new lead arrives via the API; an HTTP connector enriches it, an LLM scores its
// fit 0–100, and a branch routes hot/warm/cold leads to different follow-ups, then
// emails the owner. Demonstrates HTTP + LLM connectors, branches, and a result reducer.
//
// Connections used: enrich (generic-http), llm, composio_gmail.
//
//   flowctl def create --file examples/workflows/lead-enrichment.js
//   flowctl run start --workflow LeadEnrichment \
//     --input '{"email":"jane@acme.com","company":"Acme"}' -q

function receive(ctx) { return { received: true }; }

function enrich_req(ctx) {
  return { query_params: { domain: ctx.vars.company } };
}
function enrich_resp(ctx, response) {
  return { enrichment: response.body, employees: (response.body && response.body.employees) || 0 };
}
function enrich_failed(ctx) {
  // Enrichment is best-effort — continue with what we have.
  return { enrichment: null, employees: 0 };
}

function score_req(ctx) {
  return {
    model: "anthropic/claude-3.5-sonnet",
    messages: [
      { role: "system", content: "Score B2B lead fit 0-100. Reply with only the number." },
      { role: "user", content: "Company: " + ctx.vars.company + " (" + ctx.vars.employees + " employees), email " + ctx.vars.email },
    ],
  };
}
function score_resp(ctx, response) {
  var n = parseInt(response.choices[0].message.content, 10);
  return { score: isNaN(n) ? 0 : n };
}

function route(ctx) { return {}; }

function route_hot(ctx) { return { queue: "sales", owner: "sales@example.com" }; }
function route_nurture(ctx) { return { queue: "nurture", owner: "marketing@example.com" }; }
function route_disqualify(ctx) { return { queue: "disqualified", owner: null }; }

function notify_req(ctx) {
  return { recipient_email: ctx.vars.owner, subject: "New " + ctx.vars.queue + " lead: " + ctx.vars.company, body: "Score " + ctx.vars.score };
}
function notify_resp(ctx, response) { return { notified: response.successful === true }; }

function done(ctx) { return { status: "routed" }; }

workflow({
  name: "LeadEnrichment",
  version: "1",
  description: "Enrich, score, and route an inbound lead.",
  initial: { email: null, company: null, score: 0, queue: null, owner: null },
  inputSchema: {
    type: "object",
    properties: { email: { type: "string" }, company: { type: "string" } },
    required: ["email", "company"],
  },
  triggers: [apiTrigger("new_lead")],
  steps: [
    step("receive", { start: true, next: "enrich", label: "Receive lead" }, receive),

    connector("enrich", {
      connection: "enrich",
      operation: "lookup_company",
      instance: "default",
      requestMapper: enrich_req,
      responseCombiner: enrich_resp,
      onError: "enrich_failed",
      next: "score",
    }),
    step("enrich_failed", { next: "score", label: "Enrichment skipped" }, enrich_failed),

    connector("score", {
      connection: "llm",
      operation: "chat",
      instance: "default",
      requestMapper: score_req,
      responseCombiner: score_resp,
      next: "route",
    }),

    step("route", {
      branches: [
        branch("score >= 80", "route_hot"),
        branch("score >= 50", "route_nurture"),
        branch("true", "route_disqualify"),
      ],
    }, route),

    step("route_hot", { next: "notify", label: "Hot lead" }, route_hot),
    step("route_nurture", { next: "notify", label: "Nurture" }, route_nurture),
    step("route_disqualify", { next: "done", label: "Disqualified" }, route_disqualify),

    connector("notify", {
      connection: "composio_gmail",
      operation: "send_email",
      instance: "my-gmail",
      requestMapper: notify_req,
      responseCombiner: notify_resp,
      next: "done",
    }),

    step("done", { next: "end", label: "Done" }, done),
  ],
  result: (vars) => ({ email: vars.email, company: vars.company, score: vars.score, queue: vars.queue }),
});

Run it

flowctl def create --file examples/workflows/lead-enrichment.js
flowctl run start --workflow LeadEnrichment \
  --input '{"email":"jane@acme.com","company":"Acme"}' -q
flowctl run status <exec> -o json     # see result.score / result.queue
Best-effort enrichment

The enrich step routes to enrich_failed on error and continues with what it has, so a flaky enrichment API never blocks routing. See Errors & retries.

Variations

  • Unattended scoring uses the LLM connector. If a human is driving the run, swap it for an agent() step instead.
  • More routes. Add branches (by region, by plan) and matching follow-up connectors.
  • Write-back. Add a connector to update your CRM with the score and queue.