AI Sales Agent

This guide walks through building an AI SDR (Sales Development Representative) that qualifies inbound leads and books demo calls with your sales reps — fully autonomously.

The problem

Your AI agent can research prospects, craft emails, and qualify leads. But when it’s time to book a demo, the workflow breaks. The agent drops a Calendly link, the lead ignores it, and the opportunity goes cold.

With Slotflow, your agent books the demo directly — no links, no friction, no dropped leads.

Architecture

Inbound Lead → AI SDR Agent → Qualifies lead
→ Calls Slotflow GET /slots
→ Presents times to lead
→ Calls Slotflow POST /bookings
→ Webhook fires → CRM updated

Setup

1. Create your sales reps

Create a human for each sales rep who takes demos:

const reps = [
{ name: "Sarah Chen", email: "sarah@acme.com", role: "ae_enterprise", timezone: "America/New_York" },
{ name: "Marcus Johnson", email: "marcus@acme.com", role: "ae_midmarket", timezone: "America/Chicago" },
];
for (const rep of reps) {
const response = await fetch("https://api.slotflow.dev/v1/humans", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(rep),
});
const human = await response.json();
console.log(`Created ${human.name}: ${human.id}`);
}

2. Set availability for each rep

async function setAvailability(humanId) {
await fetch(`https://api.slotflow.dev/v1/humans/${humanId}/availability`, {
method: "PUT",
headers: {
"Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
working_days: [1, 2, 3, 4, 5], // Monday-Friday
work_start: "09:00",
work_end: "17:00",
meeting_durations: [30, 60], // 30-min intro, 60-min deep dive
}),
});
}

3. Block recurring meetings

Sales reps have team standups and pipeline reviews. Block those times:

// Block the Tuesday 2-3pm team standup for Sarah
await fetch(`https://api.slotflow.dev/v1/humans/${sarahId}/overrides`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "block",
title: "Team standup",
all_day: false,
start_date: "2026-03-01",
start_time: "14:00",
end_time: "15:00",
recurrence: { freq: "weekly", days: [2] }, // Every Tuesday
}),
});

4. Register a webhook

Get notified when bookings are confirmed so you can update your CRM:

await fetch("https://api.slotflow.dev/v1/webhooks", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://your-agent.com/webhooks/slotflow",
events: ["booking.confirmed", "booking.cancelled"],
}),
});

Agent booking flow

This is the core function your AI agent calls when it’s ready to book a demo:

async function bookDemo({ leadId, conversationId, leadName, leadEmail, repId }) {
const API_KEY = process.env.SLOTFLOW_API_KEY;
const BASE = "https://api.slotflow.dev/v1";
const headers = {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
};
// Step 1: Get available slots for the next 5 business days
const today = new Date();
const nextWeek = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000);
const dateFrom = today.toISOString().split("T")[0];
const dateTo = nextWeek.toISOString().split("T")[0];
const slotsRes = await fetch(
`${BASE}/humans/${repId}/slots?date_from=${dateFrom}&date_to=${dateTo}&duration=30`,
{ headers }
);
const { slots, timezone } = await slotsRes.json();
if (slots.length === 0) {
return { success: false, reason: "No available slots this week" };
}
// Step 2: Pick the best slot (your agent's logic here)
// For example: earliest available, or match lead's timezone preference
const selectedSlot = slots[0];
// Step 3: Book it with metadata for workflow tracking
const bookingRes = await fetch(`${BASE}/bookings`, {
method: "POST",
headers,
body: JSON.stringify({
human_id: repId,
starts_at: selectedSlot.starts_at,
duration: 30,
attendee_name: leadName,
attendee_email: leadEmail,
metadata: {
lead_id: leadId,
conversation_id: conversationId,
source: "ai_sdr",
qualified_at: new Date().toISOString(),
},
}),
});
// Step 4: Handle the response
if (bookingRes.status === 201) {
const booking = await bookingRes.json();
return { success: true, booking };
}
if (bookingRes.status === 409) {
// Slot was taken between query and booking — retry with next slot
return bookDemo({ leadId, conversationId, leadName, leadEmail, repId });
}
const error = await bookingRes.json();
return { success: false, reason: error.error.message };
}

Handling race conditions

When multiple agents are booking simultaneously, a slot might get taken between your GET /slots call and your POST /bookings call. Slotflow returns 409 SLOT_UNAVAILABLE when this happens.

Build retry logic into your agent:

async function bookWithRetry({ repId, duration, leadName, leadEmail, metadata, maxRetries = 3 }) {
const headers = {
"Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
"Content-Type": "application/json",
};
for (let attempt = 0; attempt < maxRetries; attempt++) {
// Fresh slot query on each attempt
const dateFrom = new Date().toISOString().split("T")[0];
const dateTo = new Date(Date.now() + 7 * 86400000).toISOString().split("T")[0];
const { slots } = await fetch(
`https://api.slotflow.dev/v1/humans/${repId}/slots?date_from=${dateFrom}&date_to=${dateTo}&duration=${duration}`,
{ headers }
).then(r => r.json());
if (slots.length === 0) break;
const res = await fetch("https://api.slotflow.dev/v1/bookings", {
method: "POST",
headers,
body: JSON.stringify({
human_id: repId,
starts_at: slots[attempt % slots.length].starts_at, // Try different slots
duration,
attendee_name: leadName,
attendee_email: leadEmail,
metadata,
}),
});
if (res.status === 201) return await res.json();
if (res.status !== 409) break; // Only retry on slot conflicts
}
return null; // All retries exhausted
}

Webhook handler

When a booking is confirmed, Slotflow POSTs to your webhook URL. Use this to update your CRM:

app.post("/webhooks/slotflow", (req, res) => {
const { event, data } = req.body;
if (event === "booking.confirmed") {
// Update CRM with the booked demo
crm.updateLead(data.metadata.lead_id, {
status: "demo_scheduled",
demo_time: data.starts_at,
demo_rep: data.human_id,
booking_id: data.id,
});
}
if (event === "booking.cancelled") {
// Re-queue lead for rebooking
crm.updateLead(data.metadata.lead_id, {
status: "needs_rebooking",
});
}
res.sendStatus(200); // Always respond 200 quickly
});

Multi-rep routing

If you have multiple sales reps, your agent can find the first available one:

async function findFirstAvailableRep(repIds, dateFrom, dateTo, duration) {
const headers = { "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}` };
for (const repId of repIds) {
const { slots } = await fetch(
`https://api.slotflow.dev/v1/humans/${repId}/slots?date_from=${dateFrom}&date_to=${dateTo}&duration=${duration}`,
{ headers }
).then(r => r.json());
if (slots.length > 0) {
return { repId, slots };
}
}
return null; // No reps available
}

Key takeaways

  • Use metadata to pass lead context through the booking → your webhook handler can update your CRM with all the context it needs
  • Handle 409 errors with retry logic — race conditions are expected in high-volume environments
  • Block recurring meetings with schedule overrides so your agent never double-books internal time
  • Query slots fresh before each booking attempt to minimize conflicts