Recently, one of my customers asked me to design a scheduling function for them. Here were their requirements:
- Both Studio Flows and Web Chat had to use the same function
- The Function needed to handle holidays, as well as days with irregular hours of operation
- The customer wanted to play a special message in the IVR during the holiday season. The scheduling function has to be versatile enough to know if it's holiday season or not
- The customer will eventually operate in 12 different time zones, the function had to be market and time-zone aware
- Functions v2 must be used, so they can include it in their CI/CD pipeline
JSON file for the schedule
Here is a JSON file that I came up with that will meet my customer's requirements:
{
"holidays": {
"12/25/2019": {
"description": "Christmas"
}
},
"partialDays": {
"12/26/2019": {
"begin": "10:00:00",
"end": "14:00:00",
"description": "Day after Christmas"
}
},
"regularHours": {
"Monday": {
"begin": "07:00:00",
"end": "20:00:00"
},
"Tuesday": {
"begin": "13:00:00",
"end": "20:00:00"
},
"Wednesday": {
"begin": "07:00:00",
"end": "20:00:00"
},
"Thursday": {
"begin": "07:00:00",
"end": "20:00:00"
},
"Friday": {
"begin": "07:00:00",
"end": "15:00:00"
},
"Saturday": {
"begin": null,
"end": null
},
"Sunday": {
"begin": null,
"end": null
}
}
}
We have three keys: holidays
, partialDays
and regularHours
. On Holidays, the contact center is closed the entire day. On Partial Days, we have irregular hours, and we also include the Regular hours. The evaluation will be done from top to bottom, first we check for a holiday, then for a partial day, then finally the regular schedule.
The Function's Code
I created a Function v2 which will return the following response:
{
isOpen: boolean,
isHoliday: boolean,
isPartialDay: boolean,
isRegularDay: boolean,
description: string
}
Here is my Function:
const axios = require('axios');
const Moment = require('moment-timezone');
const MomentRange = require('moment-range');
const moment = MomentRange.extendMoment(Moment);
exports.handler = function(context, event, callback) {
//create Twilio Response
let response = new Twilio.Response();
response.appendHeader('Access-Control-Allow-Origin', '*');
response.appendHeader('Access-Control-Allow-Methods', 'OPTIONS POST');
response.appendHeader('Content-Type', 'application/json');
response.appendHeader('Access-Control-Allow-Headers', 'Content-Type');
//create default response body
response.body = {
isOpen: false,
isHoliday: false,
isPartialDay: false,
isRegularDay: false,
description: ''
}
const timezone = event.timezone;
const country = event.country;
//load JSON with schedule
const jsonFile = `https://${context.DOMAIN_NAME}/${country}Schedule.json`;
axios.get(jsonFile)
.then(function (axiosResponse) {
const schedule = axiosResponse.data;
const currentDate = moment().tz(timezone).format('MM/DD/YYYY');
const isHoliday = currentDate in schedule.holidays;
const isPartialDay = currentDate in schedule.partialDays;
if (isHoliday) {
response.body.isHoliday = true;
if (typeof(schedule.holidays[currentDate].description) !== 'undefined') {
response.body.description = schedule.holidays[currentDate].description;
}
callback(null, response);
} else if (isPartialDay) {
response.body.isPartialDay = true;
if (typeof(schedule.partialDays[currentDate].description) !== 'undefined') {
response.body.description = schedule.partialDays[currentDate].description;
}
if (checkIfInRange(schedule.partialDays[currentDate].begin, schedule.partialDays[currentDate].end, timezone) === true) {
response.body.isOpen = true;
callback(null, response);
} else {
callback(null, response);
}
} else {
//regular hours
const dayOfWeek = moment().tz(timezone).format('dddd');
response.body.isRegularDay = true;
if (checkIfInRange(schedule.regularHours[dayOfWeek].begin, schedule.regularHours[dayOfWeek].end, timezone) === true) {
response.body.isOpen = true;
callback(null, response);
} else {
callback(null, response);
}
}
})
.catch(function (error) {
callback(error);
})
};
function checkIfInRange(begin, end, timezone) {
const currentDate = moment().tz(timezone).format('MM/DD/YYYY');
const now = moment().tz(timezone);
const beginMomentObject = moment.tz(`${currentDate} ${begin}`, 'MM/DD/YYYY HH:mm:ss', timezone);
const endMomentObject = moment.tz(`${currentDate} ${end}`, 'MM/DD/YYYY HH:mm:ss', timezone);
const range = moment.range(beginMomentObject, endMomentObject);
return now.within(range);
}
Tying the Studio Flow and Scheduling Function Together
How can you actually use this in a Studio flow or in Web Chat? Let's take a look at how all the components tie together. Here is a sample studio flow that utilizes our schedule:
And here's the JSON for the Studio Flow above:
{
"description": "Scheduling Example",
"states": [
{
"name": "Trigger",
"type": "InitialState",
"properties": {
"offset": {
"x": -170,
"y": -750
},
"flow_url": "https://webhooks.twilio.com/v1/Accounts/ACxxx/Flows/FWxxx"
},
"transitions": [
{
"event": "incomingMessage",
"conditions": [],
"next": null,
"uuid": "b45cf8d0-2430-4169-99e3-7be406b18ba1"
},
{
"event": "incomingCall",
"conditions": [],
"next": "FF12f317bb03189af19d3973c88371feef",
"uuid": "6dd53727-1780-434d-81cc-8836eeb6bebd"
},
{
"event": "incomingRequest",
"conditions": [],
"next": null,
"uuid": "a0f5fe33-9509-4833-9784-7a69fa5774a2"
}
],
"sid": "FF07ff2c80bc8baad0f89bfa144a7bcdcb"
},
{
"name": "sendToFlex",
"type": "SendToFlex",
"properties": {
"offset": {
"x": -270,
"y": 740
},
"workflow": "WWxxx",
"channel": "TCxxx",
"attributes": "{ \"type\": \"inbound\", \"name\": \"{{trigger.call.From}}\", \"country\":\"{{flow.variables.country}}\"}",
"timeout": null,
"priority": null,
"waitUrl": null,
"waitUrlMethod": null
},
"transitions": [
{
"event": "callComplete",
"conditions": [],
"next": null,
"uuid": "3e41c1b8-b61f-4816-bafc-fefb43f64734"
},
{
"event": "failedToEnqueue",
"conditions": [],
"next": null,
"uuid": "3b0ffcb8-72bf-4742-88f3-0d32e0e26672"
},
{
"event": "callFailure",
"conditions": [],
"next": null,
"uuid": "95873c92-bc1a-4fd2-a918-2dbe38f5d06d"
}
],
"sid": "FF58e723d175f629f8384d7dc9b88a226e"
},
{
"name": "isOpen",
"type": "Branch",
"properties": {
"offset": {
"x": -150,
"y": 150
},
"input": "{{flow.variables.isOpen}}"
},
"transitions": [
{
"event": "noMatch",
"conditions": [],
"next": null,
"uuid": "9c0de938-61b5-422d-a609-ded3bd47b542"
},
{
"event": "match",
"conditions": [
{
"friendly_name": "open",
"type": "equal_to",
"arguments": [
"{{flow.variables.isOpen}}"
],
"value": "true"
}
],
"next": "FF3ffe29cb56634313cd18537450ca9d99",
"uuid": "f0d478a7-598a-48c3-bdfb-b9d62b67c97a"
},
{
"event": "match",
"conditions": [
{
"friendly_name": "closed",
"type": "equal_to",
"arguments": [
"{{flow.variables.isOpen}}"
],
"value": "false"
}
],
"next": "FFae3209be55482d7fc8ce865276260373",
"uuid": "4e232914-e467-44e2-82a9-6394d6cd19a7"
}
],
"sid": "FF513154c31e8ce5b6c6fcee573640a76f"
},
{
"name": "CheckSchedule",
"type": "Webhook",
"properties": {
"offset": {
"x": -140,
"y": -310
},
"method": "GET",
"url": "https://schedule-functions-1234-dev.twil.io/get-schedule",
"body": null,
"timeout": null,
"parameters": [
{
"key": "country",
"value": "{{flow.variables.country}}"
},
{
"key": "timezone",
"value": "{{flow.variables.timezone}}"
}
],
"save_response_as": null,
"content_type": "application/x-www-form-urlencoded;charset=utf-8"
},
"transitions": [
{
"event": "success",
"conditions": [],
"next": "FF8fb816b136f379175a616887992788aa",
"uuid": "70acb9f1-3a44-4b9e-ab4e-e9e14b9920cc"
},
{
"event": "failed",
"conditions": [],
"next": null,
"uuid": "fa955390-729d-4676-bc48-6f8c629b589e"
}
],
"sid": "FFf9bcc7de357f477f8838d76e12af6c98"
},
{
"name": "setVariables",
"type": "SetVariables",
"properties": {
"offset": {
"x": -140,
"y": -540
},
"variables": [
{
"key": "country",
"value": "US",
"index": "0"
},
{
"key": "timezone",
"value": "Americas/Chicago",
"index": "1"
}
]
},
"transitions": [
{
"event": "next",
"conditions": [],
"next": "FFf9bcc7de357f477f8838d76e12af6c98",
"uuid": "a94a1754-202f-49e7-ae12-9670077b6271"
}
],
"sid": "FF12f317bb03189af19d3973c88371feef"
},
{
"name": "setScheduleVariables",
"type": "SetVariables",
"properties": {
"offset": {
"x": -140,
"y": -80
},
"variables": [
{
"key": "isOpen",
"value": "{{widgets.CheckSchedule.parsed.isOpen}}",
"index": "0"
},
{
"key": "isHoliday",
"value": "{{widgets.CheckSchedule.parsed.isHoliday}}",
"index": "1"
},
{
"key": "isPartialDay",
"value": "{{widgets.CheckSchedule.parsed.isPartialDay}}",
"index": "2"
},
{
"key": "isRegularDay",
"value": "{{widgets.CheckSchedule.parsed.isRegularDay}}",
"index": "3"
},
{
"key": "description",
"value": "{{widgets.CheckSchedule.parsed.description}}",
"index": "4"
}
]
},
"transitions": [
{
"event": "next",
"conditions": [],
"next": "FF513154c31e8ce5b6c6fcee573640a76f",
"uuid": "d337210f-eff2-4add-9782-d1edb153b740"
}
],
"sid": "FF8fb816b136f379175a616887992788aa"
},
{
"name": "playWelcomeMessage",
"type": "SayPlay",
"properties": {
"offset": {
"x": -250,
"y": 430
},
"say": "Thank you for calling. An agent will be with you shortly",
"play": null,
"voice": null,
"language": null,
"loop": 1,
"digits": null
},
"transitions": [
{
"event": "audioComplete",
"conditions": [],
"next": "FF58e723d175f629f8384d7dc9b88a226e",
"uuid": "0c82db16-3f31-400e-a19c-4abf6fe2834a"
}
],
"sid": "FF3ffe29cb56634313cd18537450ca9d99"
},
{
"name": "playClosedMessage",
"type": "SayPlay",
"properties": {
"offset": {
"x": 130,
"y": 430
},
"say": "Sorry, we are closed",
"play": null,
"voice": null,
"language": null,
"loop": 1,
"digits": null
},
"transitions": [
{
"event": "audioComplete",
"conditions": [],
"next": null,
"uuid": "3de9f9f1-9d29-4dfe-8bf2-f2df7e2cbf81"
}
],
"sid": "FFae3209be55482d7fc8ce865276260373"
}
]
}
Taking the JSON File Further
The JSON file is a crucial component of this. It is critical that the formatting is correct, so one little mistake doesn't break the flow. To help ensure the integrity of the JSON, I wrote a schema file which can be used for validation:
{
"type": "object",
"properties": {
"holidays": {
"type": "object",
"patternProperties": {
"(0[1-9]|1[012])[-\/.](0[1-9]|[12][0-9]|3[01])[-\/.](19|20)[0-9]{2}": {
"type": "object",
"properties": {
"description": { "type": "string" }
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"partialDays": {
"type": "object",
"patternProperties": {
"(0[1-9]|1[012])[-\/.](0[1-9]|[12][0-9]|3[01])[-\/.](19|20)[0-9]{2}": {
"type": "object",
"properties": {
"begin": {
"anyOf": [
{ "pattern": "^(?:([01]?\\d|2[0-3]):([0-5]?\\d):)?([0-5]?\\d)$"},
{ "type": "null" }
]
},
"end": {
"anyOf": [
{ "pattern": "^(?:([01]?\\d|2[0-3]):([0-5]?\\d):)?([0-5]?\\d)$" },
{ "type": "null" }
]
},
"description": { "type": "string" }
},
"required": ["begin", "end"],
"additionalProperties": false
}
}
},
"regularHours": {
"type": "object",
"patternProperties": {
"(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)": {
"type": "object",
"properties": {
"begin": {
"anyOf": [
{ "pattern": "^(?:([01]?\\d|2[0-3]):([0-5]?\\d):)?([0-5]?\\d)$"},
{ "type": "null" }
]
},
"end": {
"anyOf": [
{ "pattern": "^(?:([01]?\\d|2[0-3]):([0-5]?\\d):)?([0-5]?\\d)$" },
{ "type": "null" }
]
},
"description": { "type": "string" }
},
"required": ["begin", "end"],
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
That looks a little less scary. Here is a great tool I found for validating a JSON document against a schema: https://jsonschemalint.com/#!/version/draft-07/markup/json.
And that's it! With that, you now know how to set up advanced schedules for Studio. If you want to use this Function in a Web Chat, you simply make a fetch()
request before you render the Web Chat.
We can't wait to see what you build!
Want to try Studio? Sign up and get started here.
Lehel Gyeresi is a part of the Expert Services team and works as a Contact Center Solutions Consultant. You can reach him at lgyeresi [at] twilio.com.