Automating Call Center Management with Go and Twilio TaskRouter

May 06, 2025
Written by
Oluyemi Olususi
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Automating Call Center Management with Go and Twilio TaskRouter

Effective call center management is critical for ensuring smooth customer service operations. Whether you’re running a customer support line or managing sales inquiries, directing calls to the right agent quickly and efficiently is essential for keeping your customers satisfied and your business running smoothly. With Twilio's TaskRouter, you can automate call routing based on predefined rules, ensuring each call is directed to the most suitable agent.

By integrating TaskRouter with Go, we’ll build a simple yet powerful system for managing customer service calls. From configuring a workspace and creating tasks, to handling missed calls and enabling agents to update their availability, this tutorial will walk you through the entire process.

What you'll learn

  • How to set up a TaskRouter workspace programmatically
  • How to handle incoming calls and route them to agents using TaskRouter workflows
  • How to handle missed calls by redirecting them to voicemail
  • How to enable agents to update their availability status using SMS commands

By the end of this tutorial, you’ll have a fully functional Go-based backend that integrates seamlessly with Twilio’s TaskRouter API, making call management easier than ever.

Prerequisites

To follow along, you’ll need:

  • A basic understanding of and experience with Go (version 1.18 or newer)
  • A Twilio account (free or paid). Sign up for a free trial account, if you don’t have one already
  • A Twilio phone number capable of sending SMS
  • A phone that can send and receive SMS
  • Ngrok or a similar tool to expose your local development server to the internet

Set up the project

To begin, create a new project directory, wherever you create your Go projects, and change into it by running the following commands:

mkdir go-twilio-taskrouter
cd go-twilio-taskrouter

Initialize a new Go module

Within the go-twilio-taskrouter folder, create a new Go module by running the following command.

go mod init go-twilio-taskrouter

This will create a go.mod file in the root of your project which defines the module path and lists all dependencies required by your module.

Install the project's dependencies

This project will require two packages:

Install them using the following command:

go get github.com/joho/godotenv github.com/twilio/twilio-go

Set the required environment variables

Next, create a .env file in the project directory to securely store your Twilio credentials and host URL. Then, add the following keys to the file:

TWILIO_ACCOUNT_SID=<YOUR_TWILIO_ACCOUNT_SID>
TWILIO_AUTH_TOKEN=<YOUR_TWILIO_AUTH_TOKEN>
PERSONAL_PHONE_NUMBER=<PERSONAL_PHONE_NUMBER>
HOST_URL=<NGROK_URL>
Replace the <PERSONAL_PHONE_NUMBER> placeholder with a phone number that is not your Twilio phone number. This alternative number is required later in the tutorial to update worker availability via SMS commands.

Then, run the following command from another terminal to expose your app to the internet; the app is expected to run on port 8080:

ngrok http 8080

The command will generate a Forwarding URL like "https://abcd1234.ngrok.io". Copy this URL and replace <NGROK_URL> in your . env file with the forwarding URL.

For your Twilio credentials, replace the placeholder values in .env with the details retrieved from your Twilio Console, by following these steps:

  • Log in to the Twilio Console
  • Copy the Account SID and Auth Token from the Account Info panel
  • Replace <YOUR_TWILIO_ACCOUNT_SID> and <YOUR_TWILIO_AUTH_TOKEN> with the corresponding values
Twilio credentials
Be sure to add the .env file to your .gitignore file to prevent these sensitive credentials from being tracked by Git if you publish your code to a public repository. Also, note that, each time you restart ngrok the Forwarding URL changes. Be sure to update the HOST_URL in your .env file accordingly.

Load the environment variables

Next, to load the environment variables defined in the . env file and make them accessible throughout the application, you will create a helper function. To do this, create a folder named config, and a file called config.go within it. Following that, paste the following code into the new file:

package config

import (
    "log"
    "os"
    "github.com/joho/godotenv"
)

func LoadConfig() {
    if err := godotenv.Load(); err != nil {
        log.Fatalf("Error loading .env file: %s", err.Error())
    }
}

func GetEnv(key string) string {
    value := os.Getenv(key)
    if value == "" {
        log.Fatalf("Environment variable %s is required but not set", key)
    }
    return value
}

This file handles loading environment variables, ensuring sensitive information like Twilio credentials is securely managed. In addition, the program will log an error and terminate if the file is not found.

Initialize the Twilio Client

To access Twilio services, initialise the Twilio RestClient with your Twilio credentials. To do this, create a folder named setup and a file called client.go within it, then paste the following content into the file:

package setup

import (
    "go-twilio-taskrouter/config"
    "github.com/twilio/twilio-go"
)

func InitTwilioClient() *twilio.RestClient {
    accountSid := config.GetEnv("TWILIO_ACCOUNT_SID")
    authToken := config.GetEnv("TWILIO_AUTH_TOKEN")
    return twilio.NewRestClientWithParams(twilio.ClientParams{
        Username: accountSid,
        Password: authToken,
    })
}

This helper function sets up the Twilio RestClient, allowing the application to interact with the TaskRouter API.

Configure the TaskRouter

We’ll continue by setting up a TaskRouter workspace, defining activities, and creating workers. These components form the backbone of your call routing system.

To handle the TaskRouter configuration, create a new file named workspace_setup.go in the setup folder and paste the following content in it:

package setup

import (
    "encoding/json"
    "fmt"
    "go-twilio-taskrouter/config"
    "log"
    "github.com/twilio/twilio-go"
    openapi "github.com/twilio/twilio-go/rest/taskrouter/v1"
)

func ConfigureWorkspace(client *twilio.RestClient) {
personalPhonenumber := config.GetEnv("PERSONAL_PHONE_NUMBER")
    workspace := findOrCreateWorkspace(client)
    workers := []struct {
        Name    string
        Product string
        Contact string
    }{
        {"Bob", "ProgrammableSMS", "+123456789"},
        {"TestUser", "ProgrammableVoice", personalPhonenumber},
    }
    workerSIDs := make(map[string]string)
    for _, worker := range workers {
        attributes := fmt.Sprintf(`{"products": ["%s"], "contact_uri": "%s"}`, worker.Product, worker.Contact)
        workerSIDs[worker.Contact] = findOrCreateWorker(client, *workspace.Sid, worker.Name, attributes)
    }
    taskQueues := map[string]string{
        "ProgrammableSMS":  "products HAS 'ProgrammableSMS'",
        "ProgrammableVoice": "products HAS 'ProgrammableVoice'",
    }
    queueSIDs := findOrCreateTaskQueues(client, *workspace.Sid, taskQueues)
    findOrCreateWorkflow(client, *workspace.Sid, queueSIDs)
    fmt.Println("Workspace setup completed successfully.")
}

The ConfigureWorkspace() function is responsible for the setup of a Twilio TaskRouter workspace. At a high level, it does the following:

  • Initializes the Workspace: Ensures a workspace exists by either creating a new one or retrieving an existing one using the findOrCreateWorkspace() helper.
  • Registers Workers: Defines a list of workers, each associated with a specific product and contact number. It then registers these workers via the findOrCreateWorker() helper.
  • Configures Task Queues: It sets up task queues for routing tasks based on products (e.g., SMS or Voice). The findOrCreateTaskQueues() helper ensures these queues are properly configured.
  • Defines a Workflow: Creates or retrieves a workflow that binds tasks to queues based on routing logic. The findOrCreateWorkflow() helper encapsulates this logic.

Add helper functions for workspace configuration

As mentioned in the preceding section, the ConfigureWorkspace() function relies on several helper functions to handle the detailed logic of creating or retrieving resources like the workspace, workers, task queues, and workflows.

Paste the following content at the end of the workspace_setup.go file, just after the ConfigureWorkspace(), to define those functions.

func findOrCreateWorkspace(client *twilio.RestClient) *openapi.TaskrouterV1Workspace {
    friendlyName := "Twilio Center Workspace"
    eventCallbackURL := config.GetEnv("HOST_URL") + "/callback/events"
    workspaces, err := client.TaskrouterV1.ListWorkspace(&openapi.ListWorkspaceParams{})
    if err != nil {
        log.Fatalf("Failed to list workspaces: %s", err.Error())
    }
    for _, ws := range workspaces {
        if *ws.FriendlyName == friendlyName {
            fmt.Printf("Found existing workspace: %s\n", *ws.Sid)
            return &ws
        }
    }
    workspace, err := client.TaskrouterV1.CreateWorkspace(&openapi.CreateWorkspaceParams{
        FriendlyName:    &friendlyName,
        EventCallbackUrl: &eventCallbackURL,
    })
    if err != nil {
        log.Fatalf("Failed to create workspace: %s", err.Error())
    }
    fmt.Printf("Created new workspace: %s\n", *workspace.Sid)
    return workspace
}

func findOrCreateWorker(client *twilio.RestClient, workspaceSID, name, attributes string) string {
    existingWorkers, err := client.TaskrouterV1.ListWorker(workspaceSID, &openapi.ListWorkerParams{})
    if err != nil {
        log.Fatalf("Failed to list workers: %s", err.Error())
    }
    for _, worker := range existingWorkers {
        if *worker.FriendlyName == name {
            fmt.Printf("Found existing worker '%s' with SID: %s\n", name, *worker.Sid)
            return *worker.Sid
        }
    }
    worker, err := client.TaskrouterV1.CreateWorker(workspaceSID, &openapi.CreateWorkerParams{
        FriendlyName: &name,
        Attributes:   &attributes,
    })
    if err != nil {
        log.Fatalf("Failed to create worker '%s': %s", name, err.Error())
    }
    fmt.Printf("Worker '%s' created with SID: %s\n", name, *worker.Sid)
    return *worker.Sid
}

func findOrCreateTaskQueues(client *twilio.RestClient, workspaceSID string, queues map[string]string) map[string]string {
    queueSIDs := make(map[string]string)
    existingQueues, err := client.TaskrouterV1.ListTaskQueue(workspaceSID, &openapi.ListTaskQueueParams{})
    if err != nil {
        log.Fatalf("Failed to list task queues: %s", err.Error())
    }
    for _, queue := range existingQueues {
        queueSIDs[*queue.FriendlyName] = *queue.Sid
    }
    for name, expression := range queues {
        if _, exists := queueSIDs[name]; exists {
            fmt.Printf("Found existing task queue: %s\n", name)
            continue
        }
        queue, err := client.TaskrouterV1.CreateTaskQueue(workspaceSID, &openapi.CreateTaskQueueParams{
            FriendlyName: &name,
            TargetWorkers: &expression,
        })
        if err != nil {
            log.Fatalf("Failed to create task queue: %s", err.Error())
        }
        fmt.Printf("Created new task queue: %s\n", name)
        queueSIDs[name] = *queue.Sid
    }
    return queueSIDs
}

func findOrCreateWorkflow(client *twilio.RestClient, workspaceSID string, queueSIDs map[string]string) {
    workflowName := "Tech Support Workflow"
    existingWorkflows, err := client.TaskrouterV1.ListWorkflow(workspaceSID, &openapi.ListWorkflowParams{})
    if err != nil {
        log.Fatalf("Failed to list workflows: %s", err.Error())
    }
    for _, workflow := range existingWorkflows {
        if *workflow.FriendlyName == workflowName {
            fmt.Printf("Found existing workflow: %s\n", *workflow.Sid)
            return
        }
    }
    workflowConfig := map[string]interface{}{
        "task_routing": map[string]interface{}{
            "filters": []map[string]interface{}{
                {
                    "expression": "selected_product=='ProgrammableSMS'",
                    "targets": []map[string]string{
                        {"queue": queueSIDs["ProgrammableSMS"]},
                    },
                },
                {
                    "expression": "selected_product=='ProgrammableVoice'",
                    "targets": []map[string]string{
                        {"queue": queueSIDs["ProgrammableVoice"]},
                    },
                },
            },
            "default_filter": map[string]interface{}{
                "queue": queueSIDs["ProgrammableVoice"],
                "fallback_url": config.GetEnv("HOST_URL") + "/voicemail",
            },
        },
    }
    configJSON, err := json.Marshal(workflowConfig)
    if err != nil {
        log.Fatalf("Failed to marshal workflow configuration: %s", err.Error())
    }
    workflow, err := client.TaskrouterV1.CreateWorkflow(workspaceSID, &openapi.CreateWorkflowParams{
        FriendlyName: &workflowName,
        Configuration: stringPtr(string(configJSON)),
    })
    if err != nil {
        log.Fatalf("Failed to create workflow: %s", err.Error())
    }
    fmt.Printf("Created new workflow: %s\n", *workflow.Sid)
}

func stringPtr(s string) *string {
    return &s
}

Handle incoming calls

Next, create an HTTP handler to process incoming calls, using Twilio’s TwiML to guide callers through selecting a service. To do this, create a handlers folder and a file named call.go within it. Paste the following code into the file:

package handlers

import (
    "encoding/xml"
    "fmt"
    "go-twilio-taskrouter/config"
    "go-twilio-taskrouter/setup"
    "net/http"
    "strings"
)

type VoiceResponse struct {
    XMLName xml.Name `xml:"Response"`
    Gather  struct {
        NumDigits string `xml:"numDigits,attr"`
        Action    string `xml:"action,attr"`
        Say       string `xml:"Say"`
    } `xml:"Gather"`
    Say string `xml:"Say"`
    Pause struct {
        Length string `xml:"length,attr"`
    } `xml:"Pause"`
    Hangup string `xml:"Hangup"`
}

func IncomingCall(w http.ResponseWriter, r *http.Request) {
    response := VoiceResponse{}
    response.Gather.NumDigits = "1"
    response.Gather.Action = config.GetEnv("HOST_URL") + "/enqueue"
    response.Gather.Say = "Welcome to our service. For Programmable SMS, press 1. For Programmable Voice, press 2."
    w.Header().Set("Content-Type", "application/xml")
    xml.NewEncoder(w).Encode(response)
}

func EnqueueCall(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    digits := strings.TrimSpace(r.FormValue("Digits"))
    product := ""
    workerName := ""
    client := setup.InitTwilioClient()
    workspaceSID, err := setup.GetWorkspaceSID(client, "Twilio Center Workspace")
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte(fmt.Sprintf(`<Response><Message>Error retrieving workspace: %s</Message></Response>`, err.Error())))
        return
    }
    switch digits {
    case "1":
        product = "ProgrammableSMS"
    case "2":
        product = "ProgrammableVoice"
    default:
        response := `
        <Response>
            <Say>Invalid selection. Please call again and select a valid option. Goodbye.</Say>
            <Hangup/>
        </Response>`
        w.Header().Set("Content-Type", "application/xml")
        w.Write([]byte(response))
        return      
    }
    workerSID := setup.FindAvailableWorkerBySkill(client, workspaceSID, product)
    if workerSID != "" {
        workerName = setup.GetWorkerName(client, workspaceSID, workerSID)
    }
    if workerSID == "" {
        response := fmt.Sprintf(`
        <Response>
            <Say>We are sorry, no agents are currently available for %s. Redirecting you to voicemail.</Say>
            <Redirect>%s/voicemail</Redirect>
        </Response>`, product, config.GetEnv("HOST_URL"))
        w.Header().Set("Content-Type", "application/xml")
        w.Write([]byte(response))
        return
    }
    response := fmt.Sprintf(`
    <Response>
        <Say>Thank you for calling. Please hold while we connect you to an agent skilled in %s.</Say>
        <Pause length="3"/>
        <Say>The available agent is %s.</Say>
        <Pause length="2"/>
        <Say>Thanks for calling us today. Hope you have a nice day.</Say>
        <Hangup/>
    </Response>`, product, workerName)
    w.Header().Set("Content-Type", "application/xml")
    w.Write([]byte(response))
}

The code defines the VoiceResponse struct which defines the TwiML response structure for Twilio’s Voice API, supporting fields like <Gather>, <Say>, <Pause> and <Hangup>. Next, it defines two functions, IncomingCall() and EnqueueCall(), which handle incoming calls and route them based on user input.

The IncomingCall() function generates an XML response for Twilio's Voice API, prompting callers to press "1" for Programmable SMS, or "2" for Programmable Voice. It uses the <Gather> tag to collect a single digit and forwards the input to the "/enqueue" endpoint.

The EnqueueCall() function handles the input by determining the selected product. For invalid selections, it responds with an error message and hangs up. For valid choices, it checks for available agents with the required skill using the FindAvailableWorkerBySkill() helper function.

If no agents are available, the caller is redirected to voicemail. Otherwise, the caller is connected to the agent with a brief message, pause, and the agent's name before ending the call. This setup leverages Twilio's TaskRouter and helper functions to dynamically retrieve workspace and worker details.

Add helper functions for handling calls

The EnqueueCall() function used a couple of helper functions to retrieve workspace details, find a worker skilled in the selected product, and retrieve an agent’s name. To define these helper functions, create a new file named worker_utils.go within the setup folder and paste the following code in it.

package setup

import (
    "encoding/json"
    "fmt"
    "log"
    "github.com/twilio/twilio-go"
    openapi "github.com/twilio/twilio-go/rest/taskrouter/v1"
)

func GetWorkspaceSID(client *twilio.RestClient, friendlyName string) (string, error) {
    params := &openapi.ListWorkspaceParams{}
    workspaces, err := client.TaskrouterV1.ListWorkspace(params)
    if err != nil {
        return "", fmt.Errorf("failed to list workspaces: %w", err)
    }
    for _, ws := range workspaces {
        if *ws.FriendlyName == friendlyName {
            return *ws.Sid, nil
        }
    }
    return "", fmt.Errorf("workspace '%s' not found", friendlyName)
}

func GetWorkerSID(client *twilio.RestClient, workspaceSID, phoneNumber string) (string, error) {
    params := &openapi.ListWorkerParams{}
    workers, err := client.TaskrouterV1.ListWorker(workspaceSID, params)
    if err != nil {
        return "", fmt.Errorf("failed to list workers: %w", err)
    }
    for _, worker := range workers {
        if worker.Attributes != nil {
            var attributes map[string]interface{}
            if err := json.Unmarshal([]byte(*worker.Attributes), &attributes); err == nil {
                if attributes["contact_uri"] == phoneNumber {
                    return *worker.Sid, nil
                }
            }
        }
    }
    return "", fmt.Errorf("worker with phone number '%s' not found", phoneNumber)
}

func UpdateWorkerActivity(client *twilio.RestClient, workspaceSID, workerSID, activityName string) error {
    params := &openapi.ListActivityParams{}
    activities, err := client.TaskrouterV1.ListActivity(workspaceSID, params)
    if err != nil {
        return fmt.Errorf("failed to list activities: %w", err)
    }
    var activitySID string
    for _, activity := range activities {
        if *activity.FriendlyName == activityName {
            activitySID = *activity.Sid
            break
        }
    }
    if activitySID == "" {
        return fmt.Errorf("activity '%s' not found", activityName)
    }
    _, err = client.TaskrouterV1.UpdateWorker(workspaceSID, workerSID, &openapi.UpdateWorkerParams{
        ActivitySid: &activitySID,
    })
    if err != nil {
        return fmt.Errorf("failed to update worker activity: %w", err)
    }
    fmt.Printf("Worker %s activity updated to %s\n", workerSID, activityName)
    return nil
}

func FindAvailableWorkerBySkill(client *twilio.RestClient, workspaceSID, skill string) string {
    params := &openapi.ListWorkerParams{}
    workers, err := client.TaskrouterV1.ListWorker(workspaceSID, params)
    if err != nil {
        log.Fatalf("Failed to list workers: %s", err.Error())
    }
    for _, worker := range workers {
        if worker.Attributes == nil || worker.ActivitySid == nil {
            continue
        }
        var attributes map[string]interface{}
        if err := json.Unmarshal([]byte(*worker.Attributes), &attributes); err != nil {
            log.Printf("Failed to parse worker attributes: %s", err.Error())
            continue
        }
        if products, ok := attributes["products"].([]interface{}); ok {
            for _, product := range products {
                if product == skill && *worker.ActivityName == "Available" {
                    return *worker.Sid
                }
            }
        }
    }
    return ""
}

func GetWorkerName(client *twilio.RestClient, workspaceSID, workerSID string) string {
    worker, err := client.TaskrouterV1.FetchWorker(workspaceSID, workerSID)
    if err != nil {
        log.Printf("Failed to fetch worker details: %s", err.Error())
        return "Unknown"
    }
    return *worker.FriendlyName
}

The code above defines the following helper functions:

  • GetWorkspaceSID(): This retrieves the SID of a workspace based on its friendly name. If the workspace is not found, it returns an error.
  • GetWorkerSID(): This function retrieves theSID of a worker based on their phone number. It parses the worker's attributes to find the matching contact URI. If no worker is found, it returns an error.
  • UpdateWorkerActivity(): This function updates the activity status of a worker (e.g., "Available" or "Offline") based on the specified activity name. It first retrieves the activitySID corresponding to the name and then updates the worker's activity.
  • FindAvailableWorkerBySkill(): This function finds an available worker based on a required skill (e.g., product type). It checks the worker's attributes and activity status, returning theSID of the first match found.
  • GetWorkerName(): This function fetches and returns the friendly name of a worker using theirSID. If the worker details cannot be fetched, it defaults to "Unknown".

These helper functions provide the core utilities for interacting with Twilio's TaskRouter API, enabling workspace and worker management while maintaining code modularity and clarity.

The GetWorkerSID() and UpdateWorkerActivity() functions will be used later in the tutorial. It is specifically for updating workers availability status.

Handle missed calls

To redirect unanswered calls to voicemail when agents for a specific product are offline or unavailable, create a file named voicemail.go in the handlers folder and add the following code in it:

package handlers

import "net/http"

func RedirectToVoicemail(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/xml")
    response := `
        <Response>
            <Say>We are sorry. All our agents are currently busy. Please leave a brief message after the beep. Your message will end automatically after 10 seconds, or you can press the pound key to finish.</Say>
            <Record maxLength="10" finishOnKey="#" action="/voicemail-complete" />
            <Say>Thank you for your message. Goodbye.</Say>
            <Hangup/>
        </Response>
    `
    w.Write([]byte(response))
}

func VoicemailComplete(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/xml")
    response := `
        <Response>
            <Say>Thank you for your message. Goodbye.</Say>
            <Hangup/>
        </Response>
    `
    w.Write([]byte(response))
}

The RedirectToVoicemail() function prompts callers to leave a brief message, with a limit of 10 seconds or the option to finish by pressing "#". After recording, it transitions to the VoicemailComplete() function, which thanks the caller and ends the call.

Update workers status

Workers can update their availability status by sending SMS commands (e.g., "on" to become available). To handle the SMS commands, create a file named sms.go within the handlers folder and paste the code below in it:

package handlers

import (
    "fmt"
    "go-twilio-taskrouter/setup"
    "net/http"
    "strings"
)

func UpdateWorkerStatus(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    phoneNumber := strings.TrimSpace(r.FormValue("From"))
    body := strings.TrimSpace(strings.ToLower(r.FormValue("Body")))
    client := setup.InitTwilioClient()
    workspaceSID, err := setup.GetWorkspaceSID(client, "Twilio Center Workspace")
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte(fmt.Sprintf(`<Response><Message>Error retrieving workspace: %s</Message></Response>`, err.Error())))
        return
    }
    workerSID, err := setup.GetWorkerSID(client, workspaceSID, phoneNumber)
    if err != nil {
        w.WriteHeader(http.StatusNotFound)
        w.Write([]byte(fmt.Sprintf(`<Response><Message>Error finding worker: %s</Message></Response>`, err.Error())))
        return
    }
    message := "Unrecognized command. Reply with 'on' to become available or 'off' to go offline."
    if body == "on" {
        err = setup.UpdateWorkerActivity(client, workspaceSID, workerSID, "Available")
        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            w.Write([]byte(fmt.Sprintf(`<Response><Message>Error updating worker status: %s</Message></Response>`, err.Error())))
            return
        }
        message = fmt.Sprintf("Worker %s is now available.", phoneNumber)
    } else if body == "off" {
        err = setup.UpdateWorkerActivity(client, workspaceSID, workerSID, "Offline")
        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            w.Write([]byte(fmt.Sprintf(`<Response><Message>Error updating worker status: %s</Message></Response>`, err.Error())))
            return
        }
        message = fmt.Sprintf("Worker %s is now offline.", phoneNumber)
    }
    w.Header().Set("Content-Type", "application/xml")
    w.Write([]byte(fmt.Sprintf(`<Response><Message>%s</Message></Response>`, message)))
}

This function allows workers to update their availability status via SMS. It retrieves the sender’s phone number and message body, identifies the corresponding worker in the TaskRouter workspace, and updates their status based on the message (i.e., "on" for available and "off" for offline). If any errors occur, an appropriate response is returned.

Put it all together

Create a main.go in the project’s root directory and then paste the following code in it:

package main

import (
    "go-twilio-taskrouter/config"
    "go-twilio-taskrouter/handlers"
    "go-twilio-taskrouter/setup"
    "log"
    "net/http"
)

func main() {
    config.LoadConfig()
    client := setup.InitTwilioClient()
    setup.ConfigureWorkspace(client)

    mux := http.NewServeMux()
    mux.HandleFunc("/incoming", handlers.IncomingCall)
    mux.HandleFunc("/enqueue", handlers.EnqueueCall)
    mux.HandleFunc("/sms", handlers.UpdateWorkerStatus)
    mux.HandleFunc("/voicemail", handlers.RedirectToVoicemail)
    mux.HandleFunc("/voicemail-complete", handlers.VoicemailComplete)
    log.Println("Server running on port 8080...")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

This is the entry point of the application. It initializes the Twilio configuration, defines HTTP routes, and starts the server.

Run the application

If you haven’t already exposed the application to the internet, do so now by running the following command:

ngrok http 8080
Exposing port 8080 to the internet

Copy the generated Forwarding URL from the terminal output, setting it as the value for HOST_URL in your .env file. Then, in the Twilio Console Dashboard, configure the webhooks for your Twilio phone number as follows:

  • Go to Explore Products > Phone Numbers > Manage > Active Numbers in your Twilio Console.
  • Click on the phone number that you want to use for this tutorial.
  • Under Voice Configuration:
  • Set "Configure with" to "Webhook, TwiML Bin, Function, Studio Flow, Proxy Service"
  • Set " A call comes in" to "Webhook"
  • Set the webhook URL to <your ngrok Forwardingurl>/incoming
  • Under Messaging Configuration
  • Set " Configure with" to "Webhook, TwiML Bin, Function, Studio Flow, Proxy Service"
  • Set " A call comes in" to "Webhook"
  • Set the webhook to <ngrok_url>/sms
  • Then, click Save configuration.

Once these updates are complete, you’re ready to run the application. Start it with the following command, in a new terminal tab or session:

go run main.go

The command above will run the application on port 8080 and perform the following setup automatically:

  • Create a new workspace: Sets up the core TaskRouter environment
  • Add workers: Configures workers with specific skills and contact details
  • Create task queues: Defines queues based on product types for task routing
  • Create a workflow: Establishes the logic for routing tasks to the appropriate workers
Running the application

Test the Application

Once the application is running, follow these steps to test its functionality. Dial your Twilio phone number. You will be prompted to press "1" for Programmable SMS or "2" for Programmable Voice. Follow the prompt by pressing the corresponding number on your dial pad.

Based on your selection:

  • If an agent is available, you'll hear a message mentioning the agent's name.
  • If no agents are available, the call will redirect to voicemail. Leave a brief message after the beep, which will automatically end after 10 seconds or upon pressing "#".

At the moment, all workers should be offline because they were just created. This means you will be redirected to voicemail.

Update agent availability

All the workers are initially either offline or unavailable. Check the Workers Dashboard, which you can find in the Twilio Console under Explore Products > TaskRouter > Workspaces > Twilio Center Workspace, and click on Workers for details. You will see a page similar to the screenshot below:

View workers availability status

If you have added your phone number as the test user from the workers created programmatically earlier, send an SMS from your personal phone number to your active Twilio’s phone number. Start with an invalid unrecognized command such as “Hello”:

wrong command for sms

Now, change that and send a recognized command such as “on”, to set the worker as available.

Change worker availability

That would change the status of the “Testuser” on the dashboard. With the updated worker status, call the Twilio phone number again and follow the prompt. If a worker is available for the selected product, the system will announce the worker’s name.

Note: You can also change a worker’s availability directly from the Twilio console. Go to Explore Products > TaskRouter > Workspaces > Your Workspace > Workers > select a worker and update their activity.

That's how to automate call center management with Go and Twilio's TaskRouter

This tutorial provides a foundation for building a call management system with Go and Twilio TaskRouter. From setting up a workspace to handling calls and missed interactions, you now have a functional demo that can be extended with additional features like database integration or advanced reporting.

For further enhancements, consider adding:

  • Event Logging: Implement the /callback/events endpoint to capture and log TaskRouter events. This can be useful for monitoring, debugging, and even triggering custom business workflows based on real-time updates.
  • Missed Call Management: Extend the missed call functionality by logging unanswered calls into a database and notifying agents for follow-up actions, ensuring no customer inquiry goes unresolved.

These additions can further streamline your call center operations, providing deeper insights and improving customer service efficiency.

I hope you found this tutorial helpful. Check out Twilio's official documentation to explore additional products and services that can help grow your business. The complete source code for this tutorial can be found on GitHub. Happy coding!

Oluyemi is a tech enthusiast with a telecommunications engineering background who leverages his passion for solving everyday challenges by building web and mobile software. A full-stack Software Engineer and prolific technical writer. He shares his expertise through numerous technical articles while exploring new programming languages and frameworks. You can find him at: https://www.linkedin.com/in/yemiwebby/ and https://github.com/yemiwebby.

Customer service icons created by Freepik - Flaticon.