Skip to main content
A sales automation pipeline that qualifies leads with AI, creates contacts in HubSpot, and generates personalized outreach emails. This example shows how to combine workflows with agents and connectors for a fully automated sales process.

Project Structure

sales-automation/
├── main.ts
├── workflows/
│   └── lead-to-deal.ts
├── agents/
│   ├── qualifier.ts
│   └── copywriter.ts
├── config/
│   └── settings.ts
├── .runflow/
│   └── rf.json
├── package.json
└── tsconfig.json

Step 1: Configuration

Define scoring thresholds and qualification criteria:
config/settings.ts
export const QUALIFICATION_THRESHOLD = 7; // Score >= 7 = qualified

export const LEAD_SOURCES = ['website', 'referral', 'event', 'outbound', 'partner'] as const;
export type LeadSource = (typeof LEAD_SOURCES)[number];

export const WORKFLOW_CONFIG = {
  qualifierModel: 'gpt-4o-mini',  // Cheaper model for classification
  copywriterModel: 'gpt-4o',      // Better model for content generation
};

Step 2: Specialized Agents

Each agent handles one part of the pipeline.

Lead Qualifier

Uses a cheaper model — it just needs to score and classify:
agents/qualifier.ts
import { Agent, openai } from '@runflow-ai/sdk';
import { WORKFLOW_CONFIG } from '../config/settings';

export const qualifierAgent = new Agent({
  name: 'Lead Qualifier',
  instructions: `You are a lead qualification specialist.

## Task
Analyze the lead data provided and assign a qualification score from 1 to 10.

## Scoring Criteria
- 9-10: Enterprise buyer, clear budget, immediate timeline
- 7-8: Strong fit, budget likely, near-term timeline
- 5-6: Moderate fit, unclear budget or timeline
- 3-4: Low fit, exploring options
- 1-2: Not a fit, wrong persona or market

## Response Format
Respond with valid JSON only:
{
  "score": <number>,
  "reasoning": "<brief explanation>",
  "buyerPersona": "<decision maker | influencer | end user | unknown>",
  "urgency": "<high | medium | low>"
}`,
  model: openai(WORKFLOW_CONFIG.qualifierModel),
  modelConfig: { temperature: 0 },
});

Sales Copywriter

Uses a better model for quality content:
agents/copywriter.ts
import { Agent, openai } from '@runflow-ai/sdk';
import { WORKFLOW_CONFIG } from '../config/settings';

export const copywriterAgent = new Agent({
  name: 'Sales Copywriter',
  instructions: `You are a sales email specialist.

## Task
Write a personalized sales email based on the lead profile and qualification data.

## Rules
- Keep it under 150 words
- Reference the lead's specific company and interest
- Include one clear call-to-action (schedule a demo, book a call)
- Be consultative, not pushy
- Do NOT use generic phrases like "I hope this email finds you well"

## Response Format
Respond with valid JSON:
{
  "subject": "<email subject line>",
  "body": "<email body>"
}`,
  model: openai(WORKFLOW_CONFIG.copywriterModel),
  modelConfig: { temperature: 0.7 },
});

Step 3: Workflow Definition

The workflow orchestrates the full pipeline with conditional branching:
workflows/lead-to-deal.ts
import { createWorkflow } from '@runflow-ai/sdk';
import { z } from 'zod';
import { qualifierAgent } from '../agents/qualifier';
import { copywriterAgent } from '../agents/copywriter';
import { QUALIFICATION_THRESHOLD } from '../config/settings';

export const leadToDealWorkflow = createWorkflow({
  id: 'lead-to-deal',
  inputSchema: z.object({
    leadEmail: z.string().email(),
    leadName: z.string(),
    company: z.string(),
    role: z.string().optional(),
    source: z.string(),
    notes: z.string(),
  }),
  outputSchema: z.any(),
})
  // Step 1: Qualify the lead with AI
  .agent('qualify', qualifierAgent, {
    promptTemplate: `Analyze this lead:
Name: {{input.leadName}}
Company: {{input.company}}
Role: {{input.role}}
Source: {{input.source}}
Notes: {{input.notes}}

Provide score and analysis.`,
  })

  // Step 2: Branch based on score
  .condition(
    'check-score',
    (ctx) => {
      try {
        const analysis = JSON.parse(ctx.stepResults.get('qualify').text);
        return analysis.score >= QUALIFICATION_THRESHOLD;
      } catch {
        return false;
      }
    },
    // Qualified lead path
    [
      // Create contact in HubSpot
      {
        id: 'create-contact',
        type: 'connector',
        config: {
          connector: 'hubspot',
          resource: 'contacts',
          action: 'create',
          parameters: {
            email: '{{input.leadEmail}}',
            firstname: '{{input.leadName}}',
            company: '{{input.company}}',
            jobtitle: '{{input.role}}',
            lifecyclestage: 'lead',
            lead_source: '{{input.source}}',
          },
        },
      },
      // Generate personalized email
      {
        id: 'write-email',
        type: 'agent',
        config: {
          agent: copywriterAgent,
          promptTemplate: `Write a personalized sales email:
Lead: {{input.leadName}} ({{input.role}}) at {{input.company}}
Source: {{input.source}}
Qualification: {{qualify.text}}
Notes: {{input.notes}}`,
        },
      },
    ],
    // Low score path — log and skip
    [
      {
        id: 'log-skipped',
        type: 'function',
        config: {
          execute: async (input, ctx) => {
            return {
              status: 'skipped',
              reason: 'Below qualification threshold',
              lead: input.leadName,
            };
          },
        },
      },
    ]
  )
  .build();

Step 4: Main Entry Point

Wire the workflow into main.ts with identification and metrics:
main.ts
import { identify, track } from '@runflow-ai/sdk/observability';
import { leadToDealWorkflow } from './workflows/lead-to-deal';

export async function main(input: any) {
  // Validate required fields
  if (!input?.leadEmail || !input?.leadName || !input?.company) {
    return { error: 'leadEmail, leadName, and company are required' };
  }

  // Identify by lead email
  identify(input.leadEmail);

  try {
    const result = await leadToDealWorkflow.execute({
      leadEmail: input.leadEmail,
      leadName: input.leadName,
      company: input.company,
      role: input.role || '',
      source: input.source || 'unknown',
      notes: input.notes || '',
    });

    // Parse qualification result
    let score = 0;
    try {
      const analysis = JSON.parse(result.stepResults?.qualify?.text || '{}');
      score = analysis.score || 0;
    } catch {}

    // Track sales metrics
    track('lead_processed', {
      source: input.source,
      score,
      qualified: score >= 7,
      company: input.company,
    });

    return {
      message: score >= 7
        ? `Lead ${input.leadName} qualified (score: ${score}). Contact created and email drafted.`
        : `Lead ${input.leadName} scored ${score} — below threshold. Skipped.`,
      result,
    };
  } catch (error) {
    console.error('[sales-automation] Error:', error);
    return { error: 'Failed to process lead' };
  }
}

How It Works

Lead data comes in

┌──────────────────┐
│  Qualify (AI)     │  gpt-4o-mini scores 1-10
└──────┬───────────┘

   Score >= 7?
  ╱          ╲
 Yes          No
  ↓            ↓
Create      Log & skip
HubSpot
contact

Generate
sales email
(gpt-4o)

Return result

Key Patterns

Cheap Model for Classification, Good Model for Content

Use gpt-4o-mini for tasks like scoring and classification — it’s faster and cheaper. Save gpt-4o for content generation where quality matters.

Structured JSON Responses

Tell agents to respond with valid JSON and parse it in the workflow conditions. This makes branching reliable:
// In the agent instructions
"Respond with valid JSON: { \"score\": <number>, ... }"

// In the workflow condition
.condition('check-score', (ctx) => {
  const analysis = JSON.parse(ctx.stepResults.get('qualify').text);
  return analysis.score >= 7;
})

Workflow vs Agent

This example uses a workflow because it’s a pipeline — data flows in, gets processed through steps, and comes out. There’s no conversation. Use workflows when the process is linear, not conversational.

Next Steps

Workflows

Learn more about workflows

Connectors

Integrate with HubSpot, Slack, etc.

Collections Agent

WhatsApp collections example

Best Practices

Tips for effective agents