Advanced Schedules for Twilio Studio

December 10, 2019
Written by

Schedule.png

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
}

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:

Scheduler Studio Flow

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.