AI Sales Agent

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:

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

2. Set availability for each rep

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

3. Block recurring meetings

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

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

4. Register a webhook

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

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

Agent booking flow

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

1async function bookDemo({ leadId, conversationId, leadName, leadEmail, repId }) {
2 const API_KEY = process.env.SLOTFLOW_API_KEY;
3 const BASE = "https://api.slotflow.dev/v1";
4 const headers = {
5 "Authorization": `Bearer ${API_KEY}`,
6 "Content-Type": "application/json",
7 };
8
9 // Step 1: Get available slots for the next 5 business days
10 const today = new Date();
11 const nextWeek = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000);
12 const dateFrom = today.toISOString().split("T")[0];
13 const dateTo = nextWeek.toISOString().split("T")[0];
14
15 const slotsRes = await fetch(
16 `${BASE}/humans/${repId}/slots?date_from=${dateFrom}&date_to=${dateTo}&duration=30`,
17 { headers }
18 );
19 const { slots, timezone } = await slotsRes.json();
20
21 if (slots.length === 0) {
22 return { success: false, reason: "No available slots this week" };
23 }
24
25 // Step 2: Pick the best slot (your agent's logic here)
26 // For example: earliest available, or match lead's timezone preference
27 const selectedSlot = slots[0];
28
29 // Step 3: Book it with metadata for workflow tracking
30 const bookingRes = await fetch(`${BASE}/bookings`, {
31 method: "POST",
32 headers,
33 body: JSON.stringify({
34 human_id: repId,
35 starts_at: selectedSlot.starts_at,
36 duration: 30,
37 attendee_name: leadName,
38 attendee_email: leadEmail,
39 metadata: {
40 lead_id: leadId,
41 conversation_id: conversationId,
42 source: "ai_sdr",
43 qualified_at: new Date().toISOString(),
44 },
45 }),
46 });
47
48 // Step 4: Handle the response
49 if (bookingRes.status === 201) {
50 const booking = await bookingRes.json();
51 return { success: true, booking };
52 }
53
54 if (bookingRes.status === 409) {
55 // Slot was taken between query and booking — retry with next slot
56 return bookDemo({ leadId, conversationId, leadName, leadEmail, repId });
57 }
58
59 const error = await bookingRes.json();
60 return { success: false, reason: error.error.message };
61}

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:

1async function bookWithRetry({ repId, duration, leadName, leadEmail, metadata, maxRetries = 3 }) {
2 const headers = {
3 "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
4 "Content-Type": "application/json",
5 };
6
7 for (let attempt = 0; attempt < maxRetries; attempt++) {
8 // Fresh slot query on each attempt
9 const dateFrom = new Date().toISOString().split("T")[0];
10 const dateTo = new Date(Date.now() + 7 * 86400000).toISOString().split("T")[0];
11
12 const { slots } = await fetch(
13 `https://api.slotflow.dev/v1/humans/${repId}/slots?date_from=${dateFrom}&date_to=${dateTo}&duration=${duration}`,
14 { headers }
15 ).then(r => r.json());
16
17 if (slots.length === 0) break;
18
19 const res = await fetch("https://api.slotflow.dev/v1/bookings", {
20 method: "POST",
21 headers,
22 body: JSON.stringify({
23 human_id: repId,
24 starts_at: slots[attempt % slots.length].starts_at, // Try different slots
25 duration,
26 attendee_name: leadName,
27 attendee_email: leadEmail,
28 metadata,
29 }),
30 });
31
32 if (res.status === 201) return await res.json();
33 if (res.status !== 409) break; // Only retry on slot conflicts
34 }
35
36 return null; // All retries exhausted
37}

Webhook handler

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

1app.post("/webhooks/slotflow", (req, res) => {
2 const { event, data } = req.body;
3
4 if (event === "booking.confirmed") {
5 // Update CRM with the booked demo
6 crm.updateLead(data.metadata.lead_id, {
7 status: "demo_scheduled",
8 demo_time: data.starts_at,
9 demo_rep: data.human_id,
10 booking_id: data.id,
11 });
12 }
13
14 if (event === "booking.cancelled") {
15 // Re-queue lead for rebooking
16 crm.updateLead(data.metadata.lead_id, {
17 status: "needs_rebooking",
18 });
19 }
20
21 res.sendStatus(200); // Always respond 200 quickly
22});

Multi-rep routing

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

1async function findFirstAvailableRep(repIds, dateFrom, dateTo, duration) {
2 const headers = { "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}` };
3
4 for (const repId of repIds) {
5 const { slots } = await fetch(
6 `https://api.slotflow.dev/v1/humans/${repId}/slots?date_from=${dateFrom}&date_to=${dateTo}&duration=${duration}`,
7 { headers }
8 ).then(r => r.json());
9
10 if (slots.length > 0) {
11 return { repId, slots };
12 }
13 }
14
15 return null; // No reps available
16}

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