Bottom Linear Gradient  Lines image

Learn

Resources

Article

18 min

read

Build an AI-powered Appointment Reminder Application

Using the SignalWire AI Agent to build an appointment reminder application

SignalWire Team Headshot

SignalWire

In this article

Share

Angular Gradient Image

Build it free.

Create a space and ship your first call flow in minutes.

Subscribe

An AI-powered appointment reminder system can call a patient, confirm whether they will attend, and reschedule the appointment automatically when needed. This tutorial shows a Node.js implementation using SignalWire’s Compatibility API to place reminder calls, run Answering Machine Detection (AMD), and route the call into a SignalWire AI Agent. If a machine is detected, the call flow switches to a voicemail prompt, and if a human answers, the agent confirms attendance and can reschedule by calling backend functions that fetch available time slots and update the schedule.

Building an AI Appointment Reminder App

SignalWire's new AI Agent allows you to build innovative customer service tools into your applications. When creating an application with our Compatibility API, you can easily implement an AI agent for tasks such as rescheduling appointments to create your own virtual receptionist.

In this step-by-step guide, we will build an appointment reminder application with built-in AI answering machine detection and an AI-powered virtual assistant that can reschedule appointments.

We'll break down each piece of code to explain the functionality of machine detection and the integration with SignalWire’s AI virtual agent. By following this guide, you'll gain a clear understanding of how to leverage AI to take your customer experience to the next level.

Before we begin, we assume you have:

If you're interested in a simple video demo using the Compatibility API to build voice appointment reminders, you can watch below:


Running the Application

Clone the signalwire-in-seconds repo and go to the ai-appointment-reminders folder.

Copy the .env.example file to a file named .env, and fill in the necessary information:

NGROK_TOKEN=XXXXXXXXXXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXX
PORT=3000
SIGNALWIRE_PROJECT_ID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX
SIGNALWIRE_API_TOKEN=PTXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
SIGNALWIRE_SPACE=YOUR_SPACE.signalwire.com
FROM_NUMBER=+123456789
NGROK_TOKEN=XXXXXXXXXXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXX
PORT=3000
SIGNALWIRE_PROJECT_ID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX
SIGNALWIRE_API_TOKEN=PTXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
SIGNALWIRE_SPACE=YOUR_SPACE.signalwire.com
FROM_NUMBER=+123456789
NGROK_TOKEN=XXXXXXXXXXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXX
PORT=3000
SIGNALWIRE_PROJECT_ID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX
SIGNALWIRE_API_TOKEN=PTXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
SIGNALWIRE_SPACE=YOUR_SPACE.signalwire.com
FROM_NUMBER=+123456789
NGROK_TOKEN=XXXXXXXXXXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXX
PORT=3000
SIGNALWIRE_PROJECT_ID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX
SIGNALWIRE_API_TOKEN=PTXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
SIGNALWIRE_SPACE=YOUR_SPACE.signalwire.com
FROM_NUMBER=+123456789


Build the container by running

docker build . -t reminder
docker build . -t reminder
docker build . -t reminder
docker build . -t reminder


And then start the container with

docker run -it --rm -p 3000:3000 --name reminder --env-file .env reminder
docker run -it --rm -p 3000:3000 --name reminder --env-file .env reminder
docker run -it --rm -p 3000:3000 --name reminder --env-file .env reminder
docker run -it --rm -p 3000:3000 --name reminder --env-file .env reminder


Your application is now ready to receive POST requests to the /startReminder endpoint. It expects the following data about the patient and their appointment:

copy

01

02

03

04

05

06

07

08

09

10

11

12

{ 
 name: ‘André’,
 number: +123456789,
 date: '2023-07-11',
 time: '08:30'
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

{ 
 name: ‘André’,
 number: +123456789,
 date: '2023-07-11',
 time: '08:30'
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

{ 
 name: ‘André’,
 number: +123456789,
 date: '2023-07-11',
 time: '08:30'
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

{ 
 name: ‘André’,
 number: +123456789,
 date: '2023-07-11',
 time: '08:30'
}


The application will then make a call to the patient’s number, and transfer it to the AI Agent. Should a machine be detected, the virtual agent will be made aware of it and leave a voicemail instead of trying to interact.

If a machine is not detected, the magic happens! The AI agent starts interacting with the patient to confirm they’ll be attending the appointment, and if not, work with the patient to reschedule. While doing so, the agent will check available time slots and update them accordingly.

Breaking down the code

If you want to jump straight to the code, see our signalwire-in-seconds repository on GitHub.

Setting up dependencies

We start by importing all of our dependencies:

copy

01

02

03

04

05

06

07

08

09

10

11

12

require('dotenv').config();
const ngrok = require('ngrok');
const express = require('express');
const bodyParser = require('body-parser');
const { RestClient } = require('@signalwire/compatibility-api');

let ngrok_url;

(async function () {
    ngrok_url = await ngrok.connect({ 
        authtoken: process.env.NGROK_TOKEN, 
        addr: process.env.PORT
    });
    console.log(ngrok_url);
})();

const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const client = new RestClient(
    process.env.SIGNALWIRE_PROJECT_ID,
    process.env.SIGNALWIRE_API_TOKEN,
    {
        signalwireSpaceUrl: process.env.SIGNALWIRE_SPACE
    }
);

copy

01

02

03

04

05

06

07

08

09

10

11

12

require('dotenv').config();
const ngrok = require('ngrok');
const express = require('express');
const bodyParser = require('body-parser');
const { RestClient } = require('@signalwire/compatibility-api');

let ngrok_url;

(async function () {
    ngrok_url = await ngrok.connect({ 
        authtoken: process.env.NGROK_TOKEN, 
        addr: process.env.PORT
    });
    console.log(ngrok_url);
})();

const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const client = new RestClient(
    process.env.SIGNALWIRE_PROJECT_ID,
    process.env.SIGNALWIRE_API_TOKEN,
    {
        signalwireSpaceUrl: process.env.SIGNALWIRE_SPACE
    }
);

copy

01

02

03

04

05

06

07

08

09

10

11

12

require('dotenv').config();
const ngrok = require('ngrok');
const express = require('express');
const bodyParser = require('body-parser');
const { RestClient } = require('@signalwire/compatibility-api');

let ngrok_url;

(async function () {
    ngrok_url = await ngrok.connect({ 
        authtoken: process.env.NGROK_TOKEN, 
        addr: process.env.PORT
    });
    console.log(ngrok_url);
})();

const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const client = new RestClient(
    process.env.SIGNALWIRE_PROJECT_ID,
    process.env.SIGNALWIRE_API_TOKEN,
    {
        signalwireSpaceUrl: process.env.SIGNALWIRE_SPACE
    }
);

copy

01

02

03

04

05

06

07

08

09

10

11

12

require('dotenv').config();
const ngrok = require('ngrok');
const express = require('express');
const bodyParser = require('body-parser');
const { RestClient } = require('@signalwire/compatibility-api');

let ngrok_url;

(async function () {
    ngrok_url = await ngrok.connect({ 
        authtoken: process.env.NGROK_TOKEN, 
        addr: process.env.PORT
    });
    console.log(ngrok_url);
})();

const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const client = new RestClient(
    process.env.SIGNALWIRE_PROJECT_ID,
    process.env.SIGNALWIRE_API_TOKEN,
    {
        signalwireSpaceUrl: process.env.SIGNALWIRE_SPACE
    }
);


We load the .env file to get all of our credentials, import ngrok so we can have a tunnel to our machine, import Express so we can use it to expose the various routes, and finally, we import SignalWire’s RestClient so we can create or update calls and compose compatibility XML instructions so SignalWire can run them.

Mocking Time Slots

For simplicity, we use an object that uses days as keys and then has an array with the available times:

copy

01

02

03

04

05

06

07

08

09

10

11

12

let availability = {
    "2023-07-05": [

    ],
    "2023-07-10": [
        "11:15",
        "14:30"
    ],
    "2023-07-11": [
        "13:00"
    ]
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

let availability = {
    "2023-07-05": [

    ],
    "2023-07-10": [
        "11:15",
        "14:30"
    ],
    "2023-07-11": [
        "13:00"
    ]
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

let availability = {
    "2023-07-05": [

    ],
    "2023-07-10": [
        "11:15",
        "14:30"
    ],
    "2023-07-11": [
        "13:00"
    ]
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

let availability = {
    "2023-07-05": [

    ],
    "2023-07-10": [
        "11:15",
        "14:30"
    ],
    "2023-07-11": [
        "13:00"
    ]
}


Ideally, you should build functions to fetch availability from your CRM or a database instead.

The /startReminder endpoint

This is where all calls to patients will start. Make a POST request to this endpoint with patient data and SignalWire will make a call that:

  • Gets XML Instructions from /agent

  • Runs Answering Machine Detection asynchronously and sends results to /amd so we can then make decisions to change the AI Agent’s behavior.

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/startReminder', async (req, res) => {
    console.log(req.body)

    let name = req.body.name
    let number = req.body.number
    let date = req.body.date
    let time = req.body.time

    await client.calls.create({
        url: ngrok_url + '/agent' + `?name=${name}&` + `date=${date}&` + `time=${time}`,
        to: number,
        from: process.env.FROM_NUMBER,
        machineDetection: "DetectMessageEnd",
        machineDetectionTimeout: 45,
        asyncAmd: true,
        asyncAmdStatusCallback: ngrok_url + "/amd" + `?name=${name}&` + `date=${date}&` + `time=${time}`,
        asyncAmdStatusCallbackMethod: "POST"
    });

    return res.sendStatus(200)
});

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/startReminder', async (req, res) => {
    console.log(req.body)

    let name = req.body.name
    let number = req.body.number
    let date = req.body.date
    let time = req.body.time

    await client.calls.create({
        url: ngrok_url + '/agent' + `?name=${name}&` + `date=${date}&` + `time=${time}`,
        to: number,
        from: process.env.FROM_NUMBER,
        machineDetection: "DetectMessageEnd",
        machineDetectionTimeout: 45,
        asyncAmd: true,
        asyncAmdStatusCallback: ngrok_url + "/amd" + `?name=${name}&` + `date=${date}&` + `time=${time}`,
        asyncAmdStatusCallbackMethod: "POST"
    });

    return res.sendStatus(200)
});

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/startReminder', async (req, res) => {
    console.log(req.body)

    let name = req.body.name
    let number = req.body.number
    let date = req.body.date
    let time = req.body.time

    await client.calls.create({
        url: ngrok_url + '/agent' + `?name=${name}&` + `date=${date}&` + `time=${time}`,
        to: number,
        from: process.env.FROM_NUMBER,
        machineDetection: "DetectMessageEnd",
        machineDetectionTimeout: 45,
        asyncAmd: true,
        asyncAmdStatusCallback: ngrok_url + "/amd" + `?name=${name}&` + `date=${date}&` + `time=${time}`,
        asyncAmdStatusCallbackMethod: "POST"
    });

    return res.sendStatus(200)
});

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/startReminder', async (req, res) => {
    console.log(req.body)

    let name = req.body.name
    let number = req.body.number
    let date = req.body.date
    let time = req.body.time

    await client.calls.create({
        url: ngrok_url + '/agent' + `?name=${name}&` + `date=${date}&` + `time=${time}`,
        to: number,
        from: process.env.FROM_NUMBER,
        machineDetection: "DetectMessageEnd",
        machineDetectionTimeout: 45,
        asyncAmd: true,
        asyncAmdStatusCallback: ngrok_url + "/amd" + `?name=${name}&` + `date=${date}&` + `time=${time}`,
        asyncAmdStatusCallbackMethod: "POST"
    });

    return res.sendStatus(200)
});



The /amd endpoint

As SignalWire listens to the callee’s audio, it may determine that the call was answered by a machine. If so, instead of the call continuing to run /agent’s XML instructions, we update it so it runs /leaveVoicemail’s.

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/amd', async (req, res) => {
    console.log(req.body);

    let name = req.query.name
    let date = req.query.date
    let time = req.query.time

    switch (req.body.AnsweredBy) {
        case "machine_end_beep":
        case "machine_end_silence":
        case "machine_end_other":
            await client.calls(req.body.CallSid).update({
                url: ngrok_url + "/leaveVoicemail" + `?name=${name}&` + `date=${date}&` + `time=${time}`
            })
            break;
    }
})

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/amd', async (req, res) => {
    console.log(req.body);

    let name = req.query.name
    let date = req.query.date
    let time = req.query.time

    switch (req.body.AnsweredBy) {
        case "machine_end_beep":
        case "machine_end_silence":
        case "machine_end_other":
            await client.calls(req.body.CallSid).update({
                url: ngrok_url + "/leaveVoicemail" + `?name=${name}&` + `date=${date}&` + `time=${time}`
            })
            break;
    }
})

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/amd', async (req, res) => {
    console.log(req.body);

    let name = req.query.name
    let date = req.query.date
    let time = req.query.time

    switch (req.body.AnsweredBy) {
        case "machine_end_beep":
        case "machine_end_silence":
        case "machine_end_other":
            await client.calls(req.body.CallSid).update({
                url: ngrok_url + "/leaveVoicemail" + `?name=${name}&` + `date=${date}&` + `time=${time}`
            })
            break;
    }
})

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/amd', async (req, res) => {
    console.log(req.body);

    let name = req.query.name
    let date = req.query.date
    let time = req.query.time

    switch (req.body.AnsweredBy) {
        case "machine_end_beep":
        case "machine_end_silence":
        case "machine_end_other":
            await client.calls(req.body.CallSid).update({
                url: ngrok_url + "/leaveVoicemail" + `?name=${name}&` + `date=${date}&` + `time=${time}`
            })
            break;
    }
})



The /leaveVoicemail endpoint

When this endpoint’s code runs, it means SignalWire detected a machine answered the phone, and it is now time to change from trying to interacting with the patient to leaving a message.

We create a new VoiceResponse to start crafting our XML instructions, and use the AI Noun to give the agent a prompt describing what it should do. Check out our AI best practices guide for more information on how to write effective prompts. Finally, we convert the instructions into a string and return them so SignalWire can run them on the call.

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/leaveVoicemail', (req, res) => {
    console.log(req.query)

    let name = req.query.name
    let date = req.query.date
    let time = req.query.time

    const response = new RestClient.LaML.VoiceResponse();

    const connect = response.connect();

    const ai = connect.ai();
    ai.prompt(
        { 
            confidence: 0.2,
            temperature: 0,
            bargeConfidence: 0
        },
        `You are Gordon, an assistant at Doctor Fibonacci's office. You call patients of his to confirm appointment times. In this case you're leaving ${name} (there's no need to mention their name unless it feels natural) a voicemail about their upcoming appointment on ${date}, at ${time}.
        
        Tell the patient they can call back if they need to reschedule and hang up.
        `
    );

    const instructions = response.toString()
    console.log(instructions);
    
    res.send(instructions)
});

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/leaveVoicemail', (req, res) => {
    console.log(req.query)

    let name = req.query.name
    let date = req.query.date
    let time = req.query.time

    const response = new RestClient.LaML.VoiceResponse();

    const connect = response.connect();

    const ai = connect.ai();
    ai.prompt(
        { 
            confidence: 0.2,
            temperature: 0,
            bargeConfidence: 0
        },
        `You are Gordon, an assistant at Doctor Fibonacci's office. You call patients of his to confirm appointment times. In this case you're leaving ${name} (there's no need to mention their name unless it feels natural) a voicemail about their upcoming appointment on ${date}, at ${time}.
        
        Tell the patient they can call back if they need to reschedule and hang up.
        `
    );

    const instructions = response.toString()
    console.log(instructions);
    
    res.send(instructions)
});

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/leaveVoicemail', (req, res) => {
    console.log(req.query)

    let name = req.query.name
    let date = req.query.date
    let time = req.query.time

    const response = new RestClient.LaML.VoiceResponse();

    const connect = response.connect();

    const ai = connect.ai();
    ai.prompt(
        { 
            confidence: 0.2,
            temperature: 0,
            bargeConfidence: 0
        },
        `You are Gordon, an assistant at Doctor Fibonacci's office. You call patients of his to confirm appointment times. In this case you're leaving ${name} (there's no need to mention their name unless it feels natural) a voicemail about their upcoming appointment on ${date}, at ${time}.
        
        Tell the patient they can call back if they need to reschedule and hang up.
        `
    );

    const instructions = response.toString()
    console.log(instructions);
    
    res.send(instructions)
});

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/leaveVoicemail', (req, res) => {
    console.log(req.query)

    let name = req.query.name
    let date = req.query.date
    let time = req.query.time

    const response = new RestClient.LaML.VoiceResponse();

    const connect = response.connect();

    const ai = connect.ai();
    ai.prompt(
        { 
            confidence: 0.2,
            temperature: 0,
            bargeConfidence: 0
        },
        `You are Gordon, an assistant at Doctor Fibonacci's office. You call patients of his to confirm appointment times. In this case you're leaving ${name} (there's no need to mention their name unless it feels natural) a voicemail about their upcoming appointment on ${date}, at ${time}.
        
        Tell the patient they can call back if they need to reschedule and hang up.
        `
    );

    const instructions = response.toString()
    console.log(instructions);
    
    res.send(instructions)
});



The /agent endpoint

This endpoint returns the instructions for all calls as they start, and tells the AI agent it should interact with the patient to confirm their availability. While the code might look daunting due to being extensive, most of it is setting up a prompt just as we did in the /leaveVoicemail endpoint, with the end goal being to get the conversational AI to behave exactly like we want it to.

We structure the tasks we want the AI agent to carry out in steps, as this ensures the agent will honor the call flow.

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/agent', (req, res) => {
    console.log(req.query)

    let name = req.query.name
    let date = req.query.date
    let time = req.query.time

    const response = new RestClient.LaML.VoiceResponse();

    const connect = response.connect();

    const ai = connect.ai();
    ai.prompt(
        { 
            confidence: 0.2,
            temperature: 0,
            bargeConfidence: 0
        },
        `You are Gordon, an assistant at Doctor Fibonacci's office. You call patients of his to confirm appointment times. In this case you're calling ${name} (there's no need to mention their name unless it feels natural) about their upcoming appointment on ${date}, at ${time}.
        
        # Step 1
        Remind the patient about their upcoming appointment.
        # Step 2
        Ask if they can confirm they're going to be attending.
        ## Step 2.1
        If they cannot attend, ask them for a day when they'll be available.
        ## Step 2.2
        Use the get_available_times function to get the list of available times slots.
        
        ## Step 2.3
        Have the patient pick a time slot.
        ## Step 2.4
        Thank the patient for picking a new time slot, ask them to wait while you confirm the change, stop talking, and move on to the next step.
        ## Step 2.5
        Use the update_appointment_schedule function to update the available time slots. Never skip this step.
        ## Step 2.6
        End the call without offering further help.
        `
    );

    const swaig = ai.swaig()

    const defaults = swaig.defaults()
    defaults.setWebHookURL(ngrok_url + "/functionHandler")

    const getAvailableTimes = swaig.function()
    getAvailableTimes.setName("get_available_times")
    getAvailableTimes.setArgument("The date the customer is available on, in YYYY-MM-DD format.")
    getAvailableTimes.setPurpose("To get the date available times for a particular date.")

    const scheduleAppointment = swaig.function()
    scheduleAppointment.setName("update_appointment_schedule")
    scheduleAppointment.setArgument("The new date the customer is available on, in YYYY-MM-DD format. The new time the customer is available at, in HH:MM 24h format. The old date the appointment was supposed to take place on, in YYYY-MM-DD format. The old time the appointment was supposed to take place at, in HH:MM 24h format. Separate each value with commas and without spaces.")
    scheduleAppointment.setPurpose("To update the list of available time slots once the patient agrees to reschedule.")

    const instructions = response.toString()
    console.log(instructions);
    
    res.send(instructions)
});

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/agent', (req, res) => {
    console.log(req.query)

    let name = req.query.name
    let date = req.query.date
    let time = req.query.time

    const response = new RestClient.LaML.VoiceResponse();

    const connect = response.connect();

    const ai = connect.ai();
    ai.prompt(
        { 
            confidence: 0.2,
            temperature: 0,
            bargeConfidence: 0
        },
        `You are Gordon, an assistant at Doctor Fibonacci's office. You call patients of his to confirm appointment times. In this case you're calling ${name} (there's no need to mention their name unless it feels natural) about their upcoming appointment on ${date}, at ${time}.
        
        # Step 1
        Remind the patient about their upcoming appointment.
        # Step 2
        Ask if they can confirm they're going to be attending.
        ## Step 2.1
        If they cannot attend, ask them for a day when they'll be available.
        ## Step 2.2
        Use the get_available_times function to get the list of available times slots.
        
        ## Step 2.3
        Have the patient pick a time slot.
        ## Step 2.4
        Thank the patient for picking a new time slot, ask them to wait while you confirm the change, stop talking, and move on to the next step.
        ## Step 2.5
        Use the update_appointment_schedule function to update the available time slots. Never skip this step.
        ## Step 2.6
        End the call without offering further help.
        `
    );

    const swaig = ai.swaig()

    const defaults = swaig.defaults()
    defaults.setWebHookURL(ngrok_url + "/functionHandler")

    const getAvailableTimes = swaig.function()
    getAvailableTimes.setName("get_available_times")
    getAvailableTimes.setArgument("The date the customer is available on, in YYYY-MM-DD format.")
    getAvailableTimes.setPurpose("To get the date available times for a particular date.")

    const scheduleAppointment = swaig.function()
    scheduleAppointment.setName("update_appointment_schedule")
    scheduleAppointment.setArgument("The new date the customer is available on, in YYYY-MM-DD format. The new time the customer is available at, in HH:MM 24h format. The old date the appointment was supposed to take place on, in YYYY-MM-DD format. The old time the appointment was supposed to take place at, in HH:MM 24h format. Separate each value with commas and without spaces.")
    scheduleAppointment.setPurpose("To update the list of available time slots once the patient agrees to reschedule.")

    const instructions = response.toString()
    console.log(instructions);
    
    res.send(instructions)
});

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/agent', (req, res) => {
    console.log(req.query)

    let name = req.query.name
    let date = req.query.date
    let time = req.query.time

    const response = new RestClient.LaML.VoiceResponse();

    const connect = response.connect();

    const ai = connect.ai();
    ai.prompt(
        { 
            confidence: 0.2,
            temperature: 0,
            bargeConfidence: 0
        },
        `You are Gordon, an assistant at Doctor Fibonacci's office. You call patients of his to confirm appointment times. In this case you're calling ${name} (there's no need to mention their name unless it feels natural) about their upcoming appointment on ${date}, at ${time}.
        
        # Step 1
        Remind the patient about their upcoming appointment.
        # Step 2
        Ask if they can confirm they're going to be attending.
        ## Step 2.1
        If they cannot attend, ask them for a day when they'll be available.
        ## Step 2.2
        Use the get_available_times function to get the list of available times slots.
        
        ## Step 2.3
        Have the patient pick a time slot.
        ## Step 2.4
        Thank the patient for picking a new time slot, ask them to wait while you confirm the change, stop talking, and move on to the next step.
        ## Step 2.5
        Use the update_appointment_schedule function to update the available time slots. Never skip this step.
        ## Step 2.6
        End the call without offering further help.
        `
    );

    const swaig = ai.swaig()

    const defaults = swaig.defaults()
    defaults.setWebHookURL(ngrok_url + "/functionHandler")

    const getAvailableTimes = swaig.function()
    getAvailableTimes.setName("get_available_times")
    getAvailableTimes.setArgument("The date the customer is available on, in YYYY-MM-DD format.")
    getAvailableTimes.setPurpose("To get the date available times for a particular date.")

    const scheduleAppointment = swaig.function()
    scheduleAppointment.setName("update_appointment_schedule")
    scheduleAppointment.setArgument("The new date the customer is available on, in YYYY-MM-DD format. The new time the customer is available at, in HH:MM 24h format. The old date the appointment was supposed to take place on, in YYYY-MM-DD format. The old time the appointment was supposed to take place at, in HH:MM 24h format. Separate each value with commas and without spaces.")
    scheduleAppointment.setPurpose("To update the list of available time slots once the patient agrees to reschedule.")

    const instructions = response.toString()
    console.log(instructions);
    
    res.send(instructions)
});

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/agent', (req, res) => {
    console.log(req.query)

    let name = req.query.name
    let date = req.query.date
    let time = req.query.time

    const response = new RestClient.LaML.VoiceResponse();

    const connect = response.connect();

    const ai = connect.ai();
    ai.prompt(
        { 
            confidence: 0.2,
            temperature: 0,
            bargeConfidence: 0
        },
        `You are Gordon, an assistant at Doctor Fibonacci's office. You call patients of his to confirm appointment times. In this case you're calling ${name} (there's no need to mention their name unless it feels natural) about their upcoming appointment on ${date}, at ${time}.
        
        # Step 1
        Remind the patient about their upcoming appointment.
        # Step 2
        Ask if they can confirm they're going to be attending.
        ## Step 2.1
        If they cannot attend, ask them for a day when they'll be available.
        ## Step 2.2
        Use the get_available_times function to get the list of available times slots.
        
        ## Step 2.3
        Have the patient pick a time slot.
        ## Step 2.4
        Thank the patient for picking a new time slot, ask them to wait while you confirm the change, stop talking, and move on to the next step.
        ## Step 2.5
        Use the update_appointment_schedule function to update the available time slots. Never skip this step.
        ## Step 2.6
        End the call without offering further help.
        `
    );

    const swaig = ai.swaig()

    const defaults = swaig.defaults()
    defaults.setWebHookURL(ngrok_url + "/functionHandler")

    const getAvailableTimes = swaig.function()
    getAvailableTimes.setName("get_available_times")
    getAvailableTimes.setArgument("The date the customer is available on, in YYYY-MM-DD format.")
    getAvailableTimes.setPurpose("To get the date available times for a particular date.")

    const scheduleAppointment = swaig.function()
    scheduleAppointment.setName("update_appointment_schedule")
    scheduleAppointment.setArgument("The new date the customer is available on, in YYYY-MM-DD format. The new time the customer is available at, in HH:MM 24h format. The old date the appointment was supposed to take place on, in YYYY-MM-DD format. The old time the appointment was supposed to take place at, in HH:MM 24h format. Separate each value with commas and without spaces.")
    scheduleAppointment.setPurpose("To update the list of available time slots once the patient agrees to reschedule.")

    const instructions = response.toString()
    console.log(instructions);
    
    res.send(instructions)
});



Then, we set up the get_available_times and update_appointment_schedule functions inside of the SignalWire AI Gateway with the appropriate name, argument, and purpose. We can leverage SignalWire’s AI Gateway to “break out” of the AI’s constraints and interface with other systems.

The functions we define will have a default Webhook URL they send POST requests to. In this case we use the /functionHandler endpoint, and the data the functions will send to it look like this:

copy

01

02

03

04

05

06

07

08

09

10

11

12

{
  app_name: 'laml app',
  content_type: 'text/swaig',
  version: '2.0',
  content_disposition: 'SWAIG Function',
  function: 'get_available_times',
  argument: { parsed: [], raw: '2023-07-05', substituted: '2023-07-05' },
  call_id: 'e697030f-28f9-4158-897b-8b8b45f7618a',
  argument_desc: 'The date the customer is available on, in YYYY-MM-DD format.',
  purpose: 'To get the date available times for a particular date.',
  meta_data_token: '6664a38c0d73a08dc081575a5c32594e',
  meta_data: {}
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

{
  app_name: 'laml app',
  content_type: 'text/swaig',
  version: '2.0',
  content_disposition: 'SWAIG Function',
  function: 'get_available_times',
  argument: { parsed: [], raw: '2023-07-05', substituted: '2023-07-05' },
  call_id: 'e697030f-28f9-4158-897b-8b8b45f7618a',
  argument_desc: 'The date the customer is available on, in YYYY-MM-DD format.',
  purpose: 'To get the date available times for a particular date.',
  meta_data_token: '6664a38c0d73a08dc081575a5c32594e',
  meta_data: {}
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

{
  app_name: 'laml app',
  content_type: 'text/swaig',
  version: '2.0',
  content_disposition: 'SWAIG Function',
  function: 'get_available_times',
  argument: { parsed: [], raw: '2023-07-05', substituted: '2023-07-05' },
  call_id: 'e697030f-28f9-4158-897b-8b8b45f7618a',
  argument_desc: 'The date the customer is available on, in YYYY-MM-DD format.',
  purpose: 'To get the date available times for a particular date.',
  meta_data_token: '6664a38c0d73a08dc081575a5c32594e',
  meta_data: {}
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

{
  app_name: 'laml app',
  content_type: 'text/swaig',
  version: '2.0',
  content_disposition: 'SWAIG Function',
  function: 'get_available_times',
  argument: { parsed: [], raw: '2023-07-05', substituted: '2023-07-05' },
  call_id: 'e697030f-28f9-4158-897b-8b8b45f7618a',
  argument_desc: 'The date the customer is available on, in YYYY-MM-DD format.',
  purpose: 'To get the date available times for a particular date.',
  meta_data_token: '6664a38c0d73a08dc081575a5c32594e',
  meta_data: {}
}


The most important piece of our functions is the argument, as it tells SignalWire’s AI what to send in the POST request, and exactly in what format.

The /functionHandler endpoint

As the name suggests, this endpoint handles requests from SignalWire’s AI Gateway functions, and you can adapt it based on your requirements.

If the function that was triggered is get_available_times we get the day from the argument and return the array of available times.

If the function that was triggered is update_appointment_schedule we get new and old date/time combos, and update available time slots.

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/functionHandler', (req, res) => {
    console.log(req.body);

    switch (req.body.function) {
        case 'get_available_times':
            let day = req.body.argument.raw

            if(!availability.hasOwnProperty(day)) {
                availability[day] = []
            }

            let confirmation = {
                response: JSON.stringify(availability[day])
            }

            res.send(JSON.stringify(confirmation))
            break;

        case 'update_appointment_schedule':
            let arguments = req.body.argument.raw.split(",");
            let newDate = arguments[0]
            let newTime = arguments[1]
            let oldDate = arguments[2]
            let oldTime = arguments[3]

            console.log("Time slots before updates:", availability);

            // Remove new appointment time from available times
            for (let day in availability) {
                if (availability.hasOwnProperty(newDate)) {
                    availability[newDate] = availability[newDate].filter(item => item !== newTime);
                    break;
                }
            }

            console.log("Time slots after removing the new appointment time:", availability);

            // Add old appointment time back to available times
            for (const day in availability) {
                if (availability.hasOwnProperty(oldDate)) {
                    availability[oldDate].push(oldTime);
                    break;
                }
            }

            console.log("Time slots after adding old appointment time:", availability);

            res.send(JSON.stringify({
               response: "Appointment rescheduled." 
            }))
            break;
    }

});

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/functionHandler', (req, res) => {
    console.log(req.body);

    switch (req.body.function) {
        case 'get_available_times':
            let day = req.body.argument.raw

            if(!availability.hasOwnProperty(day)) {
                availability[day] = []
            }

            let confirmation = {
                response: JSON.stringify(availability[day])
            }

            res.send(JSON.stringify(confirmation))
            break;

        case 'update_appointment_schedule':
            let arguments = req.body.argument.raw.split(",");
            let newDate = arguments[0]
            let newTime = arguments[1]
            let oldDate = arguments[2]
            let oldTime = arguments[3]

            console.log("Time slots before updates:", availability);

            // Remove new appointment time from available times
            for (let day in availability) {
                if (availability.hasOwnProperty(newDate)) {
                    availability[newDate] = availability[newDate].filter(item => item !== newTime);
                    break;
                }
            }

            console.log("Time slots after removing the new appointment time:", availability);

            // Add old appointment time back to available times
            for (const day in availability) {
                if (availability.hasOwnProperty(oldDate)) {
                    availability[oldDate].push(oldTime);
                    break;
                }
            }

            console.log("Time slots after adding old appointment time:", availability);

            res.send(JSON.stringify({
               response: "Appointment rescheduled." 
            }))
            break;
    }

});

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/functionHandler', (req, res) => {
    console.log(req.body);

    switch (req.body.function) {
        case 'get_available_times':
            let day = req.body.argument.raw

            if(!availability.hasOwnProperty(day)) {
                availability[day] = []
            }

            let confirmation = {
                response: JSON.stringify(availability[day])
            }

            res.send(JSON.stringify(confirmation))
            break;

        case 'update_appointment_schedule':
            let arguments = req.body.argument.raw.split(",");
            let newDate = arguments[0]
            let newTime = arguments[1]
            let oldDate = arguments[2]
            let oldTime = arguments[3]

            console.log("Time slots before updates:", availability);

            // Remove new appointment time from available times
            for (let day in availability) {
                if (availability.hasOwnProperty(newDate)) {
                    availability[newDate] = availability[newDate].filter(item => item !== newTime);
                    break;
                }
            }

            console.log("Time slots after removing the new appointment time:", availability);

            // Add old appointment time back to available times
            for (const day in availability) {
                if (availability.hasOwnProperty(oldDate)) {
                    availability[oldDate].push(oldTime);
                    break;
                }
            }

            console.log("Time slots after adding old appointment time:", availability);

            res.send(JSON.stringify({
               response: "Appointment rescheduled." 
            }))
            break;
    }

});

copy

01

02

03

04

05

06

07

08

09

10

11

12

app.post('/functionHandler', (req, res) => {
    console.log(req.body);

    switch (req.body.function) {
        case 'get_available_times':
            let day = req.body.argument.raw

            if(!availability.hasOwnProperty(day)) {
                availability[day] = []
            }

            let confirmation = {
                response: JSON.stringify(availability[day])
            }

            res.send(JSON.stringify(confirmation))
            break;

        case 'update_appointment_schedule':
            let arguments = req.body.argument.raw.split(",");
            let newDate = arguments[0]
            let newTime = arguments[1]
            let oldDate = arguments[2]
            let oldTime = arguments[3]

            console.log("Time slots before updates:", availability);

            // Remove new appointment time from available times
            for (let day in availability) {
                if (availability.hasOwnProperty(newDate)) {
                    availability[newDate] = availability[newDate].filter(item => item !== newTime);
                    break;
                }
            }

            console.log("Time slots after removing the new appointment time:", availability);

            // Add old appointment time back to available times
            for (const day in availability) {
                if (availability.hasOwnProperty(oldDate)) {
                    availability[oldDate].push(oldTime);
                    break;
                }
            }

            console.log("Time slots after adding old appointment time:", availability);

            res.send(JSON.stringify({
               response: "Appointment rescheduled." 
            }))
            break;
    }

});


This code sets up a POST route for the /functionHandler endpoint. It extracts the functionName from the request body and handles different function names accordingly.

Congratulations! You have successfully built an appointment reminder app with answering machine detection and AI Agent integration! With this knowledge, you can customize and enhance the app to suit your specific requirements. Experiment and integrate SignalWire into your applications for seamless and efficient voice interactions by signing up for a free trial.

Join our Community Discord to interact with the team. We can’t wait to see what you build!

Frequently Asked Questions

What does an AI-powered appointment reminder application do?

It places an outbound reminder call, confirms whether the recipient can attend, and if they cannot, it offers alternate time slots and updates the appointment schedule based on availability.

How does Answering Machine Detection (AMD) change a call flow?

AMD runs asynchronously during the call. If the callee audio indicates a machine answered, the application updates the live call so it stops trying to interact and instead runs a voicemail-focused prompt for the AI Agent.

Which webhooks and endpoints are used for the reminder flow?

The tutorial’s flow starts at /startReminder, which creates the call and points it at /agent for the initial XML instructions, and it also runs AMD and posts results to /amd. If a machine is detected, the call is updated to run /leaveVoicemail. AI Gateway function calls are handled by /functionHandler.

Bottom Linear Gradient  Lines image

The Communications Stack for What's Next

APIs built for speed. Infrastructure built for scale. AI built in from day one.

The Communications Stack for What's Next

APIs built for speed. Infrastructure built for scale. AI built in from day one.

The Communications Stack for What's Next

APIs built for speed. Infrastructure built for scale. AI built in from day one.

The Communications Stack for What's Next

APIs built for speed. Infrastructure built for scale. AI built in from day one.