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.
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:
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:
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:
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:
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