FlowDSL
Studio
Tutorials

Build a Sales Pipeline Flow

Build a CRM-style lead enrichment, scoring, and routing flow with LLM intelligence.

This tutorial builds a lead processing pipeline that enriches incoming leads with external data, scores them using an LLM, and routes them to the correct sales action based on their score.

What you'll build

flowchart TD
  A[LeadReceived\nsource] -->|durable| B[EnrichLead\nintegration]
  B -->|durable| C[ScoreLead\nllm]
  C -->|durable| D{RouteByScore\nrouter}
  D -->|hot score ≥80| E[AssignToSalesRep\naction]
  D -->|warm 40–79| F[AddToNurtureCampaign\naction]
  D -->|cold score <40| G[ArchiveLead\nterminal]
  E -->|durable| H[SendWelcomeEmail\naction]

The complete flow YAML

yaml
flowdsl: "1.0"
info:
  title: Sales Pipeline
  version: "1.0.0"
  description: |
    Processes incoming leads: enriches from Clearbit, scores with LLM,
    routes hot/warm/cold to appropriate sales actions.

nodes:
  LeadReceived:
    operationId: receive_lead
    kind: source
    summary: Receives new lead submissions from the website form or CRM webhook
    outputs:
      out: { packet: LeadPayload }

  EnrichLead:
    operationId: enrich_lead_clearbit
    kind: integration
    summary: Enriches lead data from Clearbit (company size, industry, funding)
    inputs:
      in: { packet: LeadPayload }
    outputs:
      out: { packet: EnrichedLead }
    settings:
      clearbitApiKey: "${CLEARBIT_API_KEY}"
      enrichFields: [company, person, employment]

  ScoreLead:
    operationId: llm_score_lead
    kind: llm
    summary: Scores the lead 0–100 based on ICP fit using LLM
    inputs:
      in: { packet: EnrichedLead }
    outputs:
      out: { packet: ScoredLead }
    settings:
      model: gpt-4o-mini
      temperature: 0.2
      systemPrompt: |
        You are a B2B sales qualification expert. Score this lead 0-100 based on
        ideal customer profile fit. Consider: company size (prefer 50-500 employees),
        industry fit, funding stage, and seniority of contact.

        Return JSON: {"score": 0-100, "tier": "hot|warm|cold", "reasoning": "...", "topSignals": [...]}

  RouteByScore:
    operationId: route_lead_by_score
    kind: router
    summary: Routes leads based on their ICP score
    inputs:
      in: { packet: ScoredLead }
    outputs:
      hot:
        packet: ScoredLead
        description: Score ≥80 — assign to a sales rep immediately
      warm:
        packet: ScoredLead
        description: Score 40–79 — add to nurture campaign
      cold:
        packet: ScoredLead
        description: Score <40 — archive

  AssignToSalesRep:
    operationId: assign_sales_rep
    kind: action
    summary: Assigns the lead to a sales rep via round-robin in the CRM
    inputs:
      in: { packet: ScoredLead }
    outputs:
      out: { packet: AssignedLead }
    settings:
      crmSystem: salesforce
      assignmentStrategy: round-robin
      territory: us-west

  SendWelcomeEmail:
    operationId: send_welcome_email
    kind: action
    summary: Sends a personalized welcome email to the hot lead
    inputs:
      in: { packet: AssignedLead }
    settings:
      templateId: hot-lead-welcome-v3
      senderName: "Sales Team"

  AddToNurtureCampaign:
    operationId: add_to_nurture_campaign
    kind: action
    summary: Enrolls the lead in a 6-email nurture sequence
    inputs:
      in: { packet: ScoredLead }
    settings:
      campaignId: nurture-sequence-q1-2026
      startDelay: PT24H

  ArchiveLead:
    operationId: archive_cold_lead
    kind: terminal
    summary: Archives the lead with cold classification for future re-engagement
    inputs:
      in: { packet: ScoredLead }

edges:
  # Enrich incoming lead — durable because external API call
  - from: LeadReceived
    to: EnrichLead
    delivery:
      mode: durable
      packet: LeadPayload
      idempotencyKey: "{{payload.leadId}}-enrich"
      retryPolicy:
        maxAttempts: 3
        backoff: exponential
        initialDelay: PT3S
        retryOn: [TIMEOUT, RATE_LIMITED]

  # LLM scoring — durable + idempotency (expensive, non-deterministic)
  - from: EnrichLead
    to: ScoreLead
    delivery:
      mode: durable
      packet: EnrichedLead
      idempotencyKey: "{{payload.leadId}}-score"
      retryPolicy:
        maxAttempts: 3
        backoff: exponential
        initialDelay: PT5S
        retryOn: [RATE_LIMITED, TIMEOUT]

  # Route to appropriate path
  - from: ScoreLead
    to: RouteByScore
    delivery:
      mode: durable
      packet: ScoredLead

  # Hot path — assign to sales rep immediately
  - from: RouteByScore.hot
    to: AssignToSalesRep
    delivery:
      mode: durable
      packet: ScoredLead
      idempotencyKey: "{{payload.leadId}}-assign"
      retryPolicy:
        maxAttempts: 3
        backoff: exponential
        initialDelay: PT2S

  # Welcome email for hot leads after assignment
  - from: AssignToSalesRep
    to: SendWelcomeEmail
    delivery:
      mode: durable
      packet: AssignedLead
      idempotencyKey: "{{payload.leadId}}-welcome-email"
      retryPolicy:
        maxAttempts: 3
        backoff: exponential
        initialDelay: PT2S

  # Warm path — nurture campaign
  - from: RouteByScore.warm
    to: AddToNurtureCampaign
    delivery:
      mode: durable
      packet: ScoredLead
      idempotencyKey: "{{payload.leadId}}-nurture"
      retryPolicy:
        maxAttempts: 3
        backoff: exponential
        initialDelay: PT2S

  # Cold path — archive directly (fast, idempotent)
  - from: RouteByScore.cold
    to: ArchiveLead
    delivery:
      mode: direct
      packet: ScoredLead

components:
  packets:
    LeadPayload:
      type: object
      properties:
        leadId: { type: string }
        email: { type: string, format: email }
        firstName: { type: string }
        lastName: { type: string }
        company: { type: string }
        title: { type: string }
        phone: { type: string }
        source:
          type: string
          enum: [website, referral, event, linkedin, cold-outreach]
        submittedAt: { type: string, format: date-time }
      required: [leadId, email, company, source, submittedAt]

    EnrichedLead:
      type: object
      properties:
        lead:
          $ref: "#/components/packets/LeadPayload"
        clearbit:
          type: object
          properties:
            companySize:
              type: integer
              description: Number of employees
            industry: { type: string }
            fundingStage: { type: string }
            annualRevenue: { type: number }
            technologies: { type: array, items: { type: string } }
          additionalProperties: true
        enrichedAt: { type: string, format: date-time }
      required: [lead, enrichedAt]

    ScoredLead:
      type: object
      properties:
        lead:
          $ref: "#/components/packets/EnrichedLead"
        score:
          type: integer
          minimum: 0
          maximum: 100
        tier:
          type: string
          enum: [hot, warm, cold]
        reasoning: { type: string }
        topSignals:
          type: array
          items: { type: string }
        scoredAt: { type: string, format: date-time }
      required: [lead, score, tier, scoredAt]

    AssignedLead:
      type: object
      properties:
        lead:
          $ref: "#/components/packets/ScoredLead"
        assignedTo:
          type: object
          properties:
            repId: { type: string }
            repName: { type: string }
            repEmail: { type: string, format: email }
          required: [repId, repName, repEmail]
        crmLeadId: { type: string }
        assignedAt: { type: string, format: date-time }
      required: [lead, assignedTo, assignedAt]

Delivery mode decisions

EdgeModeWhy
LeadReceived → EnrichLeaddurableExternal API call, loss unacceptable
EnrichLead → ScoreLeaddurableExpensive LLM call with idempotency
ScoreLead → RouteByScoredurableBusiness-critical classification
RouteByScore.hot → AssignToSalesRepdurableCRM write, must not duplicate
AssignToSalesRep → SendWelcomeEmaildurableEmail send, must not duplicate
RouteByScore.warm → AddToNurtureCampaigndurableCampaign enrollment, idempotent key
RouteByScore.cold → ArchiveLeaddirectFast, idempotent, no external calls

Next steps

Copyright © 2026