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
}
Comparing Script Variants#
Edesy has no built-in A/B split. To compare scripts, split your contact list into two CSVs, run two campaigns whose agents differ in one thing (greeting, prompt or calling window), and compare connection rate, duration and outcomes in campaign analytics.
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#
- Lead Qualification - Qualify inbound leads
- Customer Support - Support agent
- Webhooks - Call completion events