Outbound Sales Agent

Build a proactive voice agent for outbound sales campaigns, follow-ups, and appointment setting.

Outbound Sales Agent

A proactive voice agent that makes outbound calls for sales campaigns, appointment setting, and follow-ups.

Configuration

{
  "agent": {
    "name": "Outbound Sales",
    "language": "en-US",

    "llmProvider": "gemini-2.5",
    "llmModel": "gemini-2.5-flash-lite",
    "llmTemperature": 0.7,

    "sttProvider": "deepgram",
    "sttModel": "nova-3",

    "ttsProvider": "cartesia",
    "ttsVoice": "95856005-0332-41b0-935f-352e296aa0df",

    "greetingMessage": "Hi {{contactName}}! This is Alex from {{companyName}}. I'm calling because {{reason}}. Do you have a quick minute?",

    "prompt": "...",

    "allowInterruptions": true
  }
}

System Prompt

You are an outbound sales representative for {{companyName}}. Your goal is to engage prospects, share value, and schedule meetings with the sales team.

## Call Objective
{{callObjective}}

## About {{companyName}}
{{companyDescription}}

## Prospect Information
- Name: {{contactName}}
- Company: {{prospectCompany}}
- Role: {{contactRole}}
- Previous Interaction: {{previousInteraction}}

## Call Script Framework

### Opening (10 seconds)
Get permission to continue the conversation.

"Hi {{contactName}}! This is Alex from {{companyName}}.
I'm reaching out because [reason].
Do you have a quick minute?"

### Value Hook (20 seconds)
Share one compelling insight or benefit.

"The reason I'm calling is that we've been helping companies like [similar company]
[achieve specific result]. I thought it might be relevant for {{prospectCompany}}."

### Engagement Question
Ask an open-ended question to start dialogue.

"I'm curious - how is your team currently handling [problem area]?"

### Value Proposition
Based on their response, share relevant benefits.

### Close
Ask for a specific next step.

"Would you be open to a 15-minute call next week to show you how this works?"

## Objection Handling

### "I'm busy right now"
"Totally understand! What would be a better time for a 2-minute call?
I can share something that saved [customer] 10 hours/week."

### "Not interested"
"I hear you. Before I go - just curious, is it the timing or
the solution that doesn't fit right now?"

### "Send me an email"
"Happy to! What specifically would be most useful to include?
That way I can tailor it to what matters to you."

### "We already have a solution"
"Great that you're already addressing this! What made you go with [competitor]?
We often work alongside existing tools."

### "How did you get my number?"
"Fair question! [Explain source - trade show, referral, public directory].
If this isn't a good fit, I completely understand. Would you prefer I remove you?"

## Tone Guidelines
- Confident but not pushy
- Respectful of their time
- Focused on value, not features
- Curious and listening
- Professional but personable

## What NOT to Do
- Don't read a script robotically
- Don't push after clear rejection
- Don't make promises you can't keep
- Don't talk more than 50% of the time
- Don't ignore their responses

Campaign Setup

Initiating Outbound Calls

func runOutboundCampaign(campaignID string) {
    campaign, _ := db.GetCampaign(campaignID)
    contacts := db.GetCampaignContacts(campaignID)

    for _, contact := range contacts {
        // Check calling hours (9 AM - 6 PM local time)
        if !isWithinCallingHours(contact.Timezone) {
            scheduleForLater(contact)
            continue
        }

        // Prepare variables
        variables := map[string]string{
            "contactName":         contact.Name,
            "prospectCompany":     contact.Company,
            "contactRole":         contact.Role,
            "companyName":         campaign.CompanyName,
            "companyDescription":  campaign.CompanyDescription,
            "callObjective":       campaign.Objective,
            "reason":              campaign.CallReason,
            "previousInteraction": contact.LastInteraction,
        }

        // Make call
        initiateCall(campaign.AgentID, contact.Phone, variables)

        // Respect rate limits and calling regulations
        time.Sleep(campaign.DelayBetweenCalls)
    }
}

API Call

curl -X POST https://voice-agent.edesy.in/api/v1/calls \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "agent_id": "outbound_sales",
    "to": "+1234567890",
    "variables": {
      "contactName": "Sarah",
      "prospectCompany": "Acme Corp",
      "contactRole": "VP of Operations",
      "companyName": "TechCo",
      "callObjective": "schedule demo",
      "reason": "I saw your recent post about scaling challenges",
      "previousInteraction": "Downloaded case study last week"
    },
    "webhook_url": "https://your-server.com/webhooks/call-complete"
  }'

Tools

Schedule Callback

{
  "type": "function",
  "function": {
    "name": "schedule_callback",
    "description": "Schedule a callback at a better time",
    "parameters": {
      "type": "object",
      "properties": {
        "contact_id": { "type": "string" },
        "callback_time": {
          "type": "string",
          "description": "Preferred callback time (ISO 8601)"
        },
        "reason": {
          "type": "string",
          "description": "Why they want a callback"
        }
      },
      "required": ["contact_id", "callback_time"]
    }
  }
}

Book Meeting

{
  "type": "function",
  "function": {
    "name": "book_meeting",
    "description": "Book a meeting with the sales team",
    "parameters": {
      "type": "object",
      "properties": {
        "contact_id": { "type": "string" },
        "meeting_type": {
          "type": "string",
          "enum": ["discovery", "demo", "consultation"]
        },
        "preferred_times": {
          "type": "array",
          "items": { "type": "string" }
        },
        "attendees": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Additional attendees from prospect's side"
        },
        "notes": { "type": "string" }
      },
      "required": ["contact_id", "meeting_type"]
    }
  }
}

Update Contact Status

{
  "type": "function",
  "function": {
    "name": "update_contact",
    "description": "Update contact status and notes",
    "parameters": {
      "type": "object",
      "properties": {
        "contact_id": { "type": "string" },
        "status": {
          "type": "string",
          "enum": ["interested", "callback", "not_interested", "no_answer", "wrong_number", "do_not_call"]
        },
        "notes": { "type": "string" },
        "next_action": { "type": "string" }
      },
      "required": ["contact_id", "status"]
    }
  }
}

Send Follow-up Email

{
  "type": "function",
  "function": {
    "name": "send_email",
    "description": "Send follow-up email after call",
    "parameters": {
      "type": "object",
      "properties": {
        "contact_id": { "type": "string" },
        "template": {
          "type": "string",
          "enum": ["intro", "meeting_invite", "resources", "case_study"]
        },
        "personalization": { "type": "string" }
      },
      "required": ["contact_id", "template"]
    }
  }
}

Tool Handlers

func handleBookMeeting(params map[string]any) (string, error) {
    contactID := params["contact_id"].(string)
    meetingType := params["meeting_type"].(string)

    // Get available slots from sales team calendar
    slots, _ := calendar.GetTeamAvailability(3) // 3 options

    if len(slots) == 0 {
        return "Let me have a colleague reach out with some times. What's the best email to send that to?", nil
    }

    // Format for speech
    response := "Perfect! I have a few times available: "
    for i, slot := range slots {
        response += formatSlotForSpeech(slot)
        if i < len(slots)-1 {
            response += ", or "
        }
    }
    response += ". Which works best?"

    return response, nil
}

func handleSendEmail(params map[string]any) (string, error) {
    contactID := params["contact_id"].(string)
    template := params["template"].(string)

    contact, _ := db.GetContact(contactID)

    email := buildEmail(template, contact, params["personalization"])
    err := emailService.Send(email)
    if err != nil {
        return "I'll make sure you get that email.", err
    }

    return fmt.Sprintf("Done! I just sent that to %s. You should see it shortly.", contact.Email), nil
}

Compliance & Regulations

Do Not Call (DNC) Handling

func preCallCheck(contact *Contact) error {
    // Check internal DNC list
    if db.IsOnDNCList(contact.Phone) {
        return errors.New("contact is on DNC list")
    }

    // Check calling hours
    if !isWithinCallingHours(contact.Timezone) {
        return errors.New("outside calling hours")
    }

    // Check frequency cap
    callsToday := db.GetCallCountToday(contact.Phone)
    if callsToday >= 3 {
        return errors.New("frequency cap reached")
    }

    return nil
}

Calling Hours

var callingHours = map[string]struct{ start, end int }{
    "US": {9, 21},  // 9 AM - 9 PM local
    "EU": {9, 20},  // 9 AM - 8 PM local (GDPR consideration)
    "IN": {9, 21},  // 9 AM - 9 PM local
}

func isWithinCallingHours(timezone string) bool {
    loc, _ := time.LoadLocation(timezone)
    localTime := time.Now().In(loc)
    hour := localTime.Hour()

    region := getRegion(timezone)
    hours := callingHours[region]

    return hour >= hours.start && hour < hours.end
}

DNC Request Handling

If the contact asks to be removed or not called again:

"Absolutely, I've made a note and you won't receive any more calls from us.
I apologize for any inconvenience. Have a great day!"

→ Immediately mark as do_not_call
→ Add to DNC list
→ End call gracefully

Call Outcomes

type CallOutcome string

const (
    OutcomeConnected      CallOutcome = "connected"
    OutcomeVoicemail      CallOutcome = "voicemail"
    OutcomeNoAnswer       CallOutcome = "no_answer"
    OutcomeBusy           CallOutcome = "busy"
    OutcomeInvalidNumber  CallOutcome = "invalid_number"
)

type CallResult struct {
    ContactID        string
    Outcome          CallOutcome
    Duration         time.Duration
    Status           string // interested, callback, not_interested, etc.
    MeetingScheduled bool
    CallbackTime     *time.Time
    Notes            string
    Transcript       string
}

func handleCallComplete(result CallResult) {
    // Update CRM
    crm.UpdateContact(result.ContactID, result)

    // Track metrics
    metrics.Increment("outbound.calls.total")
    metrics.Increment("outbound.outcome." + string(result.Outcome))

    if result.MeetingScheduled {
        metrics.Increment("outbound.meetings.booked")
        notifySalesRep(result)
    }

    // Schedule follow-ups
    if result.Status == "callback" && result.CallbackTime != nil {
        scheduleCallback(result.ContactID, *result.CallbackTime)
    }
}

Voicemail Handling

When call goes to voicemail:

{
  "voicemail": {
    "enabled": true,
    "detection": "auto",
    "message": "Hi {{contactName}}, this is Alex from {{companyName}}. I was calling about {{reason}}. I'd love to connect briefly - you can reach me at 555-123-4567 or I'll try you again {{callbackDay}}. Looking forward to speaking with you!"
  }
}
func handleVoicemail(contact *Contact, campaign *Campaign) {
    voicemailMsg := buildVoicemailMessage(contact, campaign)

    // Leave voicemail via TTS
    tts.Speak(voicemailMsg)

    // Update contact
    db.UpdateContact(contact.ID, ContactUpdate{
        Status:       "voicemail_left",
        NextAction:   "follow_up_call",
        NextActionAt: time.Now().Add(48 * time.Hour),
    })

    // Optionally send follow-up email
    sendVoicemailFollowupEmail(contact)
}

Campaign Analytics

type CampaignMetrics struct {
    TotalCalls        int
    Connected         int
    ConnectionRate    float64
    MeetingsBooked    int
    BookingRate       float64
    AverageCallTime   time.Duration
    CallsPerHour      float64
    BestTimeToCall    string
    TopObjections     []string
}

func getCampaignMetrics(campaignID string) *CampaignMetrics {
    calls := db.GetCampaignCalls(campaignID)

    metrics := &CampaignMetrics{
        TotalCalls: len(calls),
    }

    for _, call := range calls {
        if call.Outcome == "connected" {
            metrics.Connected++
        }
        if call.MeetingScheduled {
            metrics.MeetingsBooked++
        }
        metrics.AverageCallTime += call.Duration
    }

    metrics.ConnectionRate = float64(metrics.Connected) / float64(metrics.TotalCalls)
    metrics.BookingRate = float64(metrics.MeetingsBooked) / float64(metrics.Connected)
    metrics.AverageCallTime /= time.Duration(len(calls))

    return metrics
}

A/B Testing

Test different approaches:

{
  "campaign": {
    "id": "summer_outreach",
    "variants": [
      {
        "id": "direct",
        "weight": 50,
        "greetingMessage": "Hi {{contactName}}! I have a quick question about your team's approach to [topic]. Got 30 seconds?"
      },
      {
        "id": "value_first",
        "weight": 50,
        "greetingMessage": "Hi {{contactName}}! We just helped a company like yours save $50K. Thought it might be relevant - do you have a minute?"
      }
    ]
  }
}

Best Practices

1. Respect Their Time

Opening should be < 15 seconds.
Ask permission: "Do you have a quick minute?"
If not: "When would be better? I'll call back then."

2. Lead with Value

❌ "I'm calling to tell you about our product..."
✅ "I'm calling because we helped [similar company] reduce [problem] by 40%..."

3. Handle Rejection Gracefully

"I appreciate you taking my call. If anything changes, feel free to reach out."
→ Update status, respect their decision, don't push

4. Follow Up Appropriately

followUpRules := map[string]FollowUp{
    "interested": {
        Delay:  24 * time.Hour,
        Action: "send_calendar_link",
    },
    "callback": {
        Delay:  time.Duration(0), // Use scheduled time
        Action: "call_again",
    },
    "voicemail": {
        Delay:  48 * time.Hour,
        Action: "try_again",
    },
    "not_interested": {
        Delay:  90 * 24 * time.Hour, // 90 days
        Action: "nurture_email",
    },
}

Next Steps