> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runflow.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Sales Automation with Workflow

> Automate lead qualification, deal creation, and personalized outreach using workflows

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:

```typescript config/settings.ts theme={null}
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:

```typescript agents/qualifier.ts theme={null}
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:

```typescript agents/copywriter.ts theme={null}
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:

```typescript workflows/lead-to-deal.ts theme={null}
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:

```typescript main.ts theme={null}
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:

```typescript theme={null}
// 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

<CardGroup cols={2}>
  <Card title="Workflows" icon="diagram-project" href="/core-concepts/workflows">
    Learn more about workflows
  </Card>

  <Card title="Connectors" icon="plug" href="/core-concepts/connectors">
    Integrate with HubSpot, Slack, etc.
  </Card>

  <Card title="Collections Agent" icon="money-bill" href="/use-cases/collections-agent">
    WhatsApp collections example
  </Card>

  <Card title="Best Practices" icon="lightbulb" href="/best-practices">
    Tips for effective agents
  </Card>
</CardGroup>
