The Amazon Connect Contact Control Panel Revisited

After writing the last article on the Amazon Connect Contact Control Panel, I upgraded the Streams API (application programming interface) script and spent a lot more time testing, debugging, and updating my code. I found new features in the updated Streams API and I found bugs and errors in my code. Let’s look at the changes…

Logging In

There is still no way to automatically log in to the Contact Control Panel (CCP). Amazon requires the CCP open in another window and present the agent with a login screen. What has changed is a new option to automatically close that window once the agent logs in. Here is the initialization code in the WebAgent phone script:

    connect.core.initCCP(target, {
        ccpUrl:                 ccpUrl,     /*REQUIRED*/
        loginPopup:             true,       /*optional, default TRUE*/
        loginUrl:               null,       /*optional*/
        loginPopupAutoClose:    true,       /*optional*/
        region:                 null,       /*REQUIRED for chat*/
        softphone: {                        /*optional*/
            allowFramedSoftphone: true,     /*optional*/
            disableRingtone:    false,      /*optional*/
            ringtoneUrl:        null        /*optional*/
        }
    });

The new option, “loginPopupAutoClose” set to true does the trick eliminating the code hack I used before…

 connect.agent(function(agent) {
            // close the ccp login window when agent is available
            var w = window.open('', connect.MasterTopics.LOGIN_POPUP);
            if (w) {w.close();}

Amazon Connect CCP Login

API Callbacks

The Streams API also has a few new callbacks for both the agent and contact objects…

The connect.agent() method is called after the agent successfully logs in to the CCP:

        connect.agent(function(agent) {             // when the agent is initialized
            awsConnect.setAgentLogin(agent);
            // catch agent events
            agent.onContactPending(function(a){return awsConnect.onAgentCallPending(a);});
            agent.onRefresh(function(a){return awsConnect.onAgentRefresh(a);});
            agent.onStateChange(function(c){return awsConnect.onAgentState(c);});
            agent.onRoutable(function(a){return awsConnect.onAgentRoutable(a);});
            agent.onNotRoutable(function(a){return awsConnect.onAgentNotRoutable(a);});
            agent.onOffline(function(a){return awsConnect.onAgentOffline(a);});
            agent.onAfterCallWork(function(a){return awsConnect.onAgentAfterCall(a);});
            agent.onError(function(a){return awsConnect.onAgentError(a);}); // becomes not-routable
            agent.onSoftphoneError(function(a){return awsConnect.onAgentSoftphoneError(a);}); // becomes not-routable
        });

The connect.contact() method is called with each new contact (or call):

        connect.contact(function(contact) {         // for each newly detected agent contact
            // catch contact events
            contact.onRefresh(function(call){return awsConnect.onCallRefresh(call)});
            contact.onIncoming(function(call){return awsConnect.onCallIncoming(call)});
            contact.onPending(function(call){return awsConnect.onCallPending(call)});
            contact.onConnecting(function(call){return awsConnect.onCallConnecting(call)});
            contact.onAccepted(function(call){return awsConnect.onCallAccepted(call)});
            contact.onMissed(function(call){return awsConnect.onCallMissed(call)});
            contact.onConnected(function(call){return awsConnect.onCallConnected(call)});
            contact.onEnded(function(call){return awsConnect.onCallEnded(call)});
        });

The details for these callbacks can be found in the Amazon Connect Streams Documentation.

For the Agent, I added onContactPending(), onError(), and onSoftphoneError(). I’ve yet to see an instance where onContactPending() actually gets called. I thought I could trap this call to alert the agent of a pending call but, obviously, not. I added the error methods because the phone goes offline after an error thus preventing any calls afterwards. In my code, I correct this and put the phone back into a routable state.

For the Contact, I added onPending(), onConnecting(), and onMissed(). After some testing in different scenarios, I found inconsistencies with when these methods are called - internally inconsistent and inconsistent with the documentation.

I would expect a sequence close to this as a call is routed to and handled by an agent…

onIncoming() -> onPending() -> on Accepted() -> onConnecting() -> onConnected() (or onMissed()) -> onEnded()

But not so.

For an inbound call, what we see is…

onConnecting() -> onAccepted() -> onConnected() -> onEnded()

For an outbound call…

onConnecting() -> onConnected() -> onEnded()

And for an automated callback (when a caller leaves their number for a callback in queue)…

onIncoming() -> onAccepted() -> onPending() -> onEnded() -> onConnecting() -> onConnected() -> onEnded()

The automated callback is the one that comes close to what is documented, though I’m not sure why pending comes after accepted but it is documented that way (between incoming and connecting). And what is ending between pending and connecting? That was something I had to take into account in my code… an onEnded() doesn’t necessarily mean that the call actually ended. I also found that onEnded() may be called multiple times after a call has ended.

Something I want in WebAgent is to notify the agent when a call is about to connect. Because of this inconsistency, I need to do that on both onConnecting() and onIncoming(). onConnecting() is the first event for inbound and outbound calls but for callbacks, onConnecting() doesn’t happen until after the agent has accepted the call. onIncoming() works for callbacks but doesn’t get called for inbound and outbound calls.

Not Routable States

To make matters worse, the agent state is changed inconstantly as well. The agent state determines whether an agent is available (routable) or not (not-routable). There are 4 types of states, system states, error states, routable states, and not-routable states. Even though system and error states may put the agent in an unavailable (not routable) state, they are not considered not-routable states. There are two callbacks triggered by non-system/error states in the agent object… onRoutable() and onNotRoutable(). These states have specific, valid names (or reasons). Typically, “Available” is the only routable state and not-routable states are created in the Contact Management console. System states (such as busy or pending) and error states do not trigger these callbacks and are not considered valid routable/not-routabele states but can be detected by the onStateChange() callback.

For example, when an inbound call is routed to an agent in a currently routable (available) state, onStateChange() will show the agent as “PendingBusy”, then “Busy”, then “AfterCallWork” when the call is completed and, lastly, back to “Available”. Only the “Available” state triggers the onRoutable() callback.

For outbound calls, the sequence is similar with “CallingCustomer”, then “Busy”, then “AfterCallWork” and back to “Available”.

However, automated callbacks mess this up by treating system states as not-routable triggering the onNotRoutable() callback. With callbacks we see “Pending”, “Busy”, “CallingCustomer”, “Busy”, then “AfterCallWork” and back to “Available”. The first “Pending” and “Busy” states trigger the onNotRoutable() callback even though those states are not valid not-routable states. Note that the remaining states (including the 2nd “Busy”) are seen as system states and do not trigger the onNotRoutable() callback.

This isn’t the end of the world, in my state change callbacks (onRoutable() and onNotRoutable()) I first check the state name to the list provided by the API with a type equal to connect.AgentStateType.ROUTABLE or connect.AgentStateType.NOT_ROUTABLE. If the state is not valid, I don’t act on it.

Pausing

The above directly effects how WebAgent handles pauses. When an agent is logged in but not accepting calls, they are said to be “paused” in WebAgent or not-routable in Amazon Connect. An agent may be paused for several reasons such as on break, doing clerical work, out to lunch, in training, etc. However, when the phone system is “busy” routing a call to the agent, the agent isn’t paused. I use the onRoutable() callback to take an agent “off pause” (unpause) and the onNotRoutable() callback to put an agent “on paused” (paused). WebAgent (and QueueMetrics) track the time and reason an agent is paused.

Offline

Previously, I had decided that an “Offline” state was the same as logged-out. If the agent triggered onOffline() by selecting the “Offline” status from the CCP, I would log them out of both the CCP and WebAgent.

This has changed - both out of necessity and preference. I now use “Offline” as a temporary state where the agent is not accepting calls but is not “paused” for any specific reason. One example of why I’ve done this is in the case of a missed call. If the CCP is not set to auto-answer, and a call comes in to the agent but the agent doesn’t accept the call, the agent is then left in an error/offline state and no calls will come through. However, if I immediately force the agent back to a routable/available state, the same call may likely come right back to the agent (let’s assume there’s a valid reason the agent didn’t accept the call). So I treat this state as “Offline” and force the agent back to routable/available after a short waiting period. I also use the “Offline” state to control how quickly calls may be routed to an agent after ending a call, coming off pause or when they first log in. If an agent manually puts themselves “Offline”, WebAgent will shortly put them back to routable/available. Other uses include when an agent is not on a call but searching for a customer or prospect to make an outbound call or if they are managing scheduled callbacks in WebAgent (again, about to make an outbound call), I can put them “Offline” (preventing inbound calls) until they complete their search and dial the call.

Transfers and Quick Connects

In my previous article, I outlined how WebAgent manages transfers, can access Quick Connects but can’t use them programmatically. I was wrong. Turns out this was one of the bugs I had in my code and I was incorrectly parsing (read: screwing up) the Quick Connect ARN (Amazon Resource Name) before attempting to create an Endpoint to call.

When the agent logs into the CCP, I have a method that collects the Quick Connects through the API and sends them to my server to later use in the call scripts…

    setAgentLogin : function(agent)
    {
        try {
            var config = agent.getConfiguration();                  // get the agent configuration
            agent.getEndpoints(agent.getAllQueueARNs(), {           // get the outbound quick connects
                success: function(data) {
                    config.quickConnects = data.endpoints;
                    WebNav.send('account','register',{AgentConfig: config});
                    Phone.Login();
                },
                failure: function(error) {
                    WebNav.send('account','register',{AgentConfig: config});
                    Phone.Login();
                }
            });
        } catch(e) {console.log(e.message);}
        return true;
    },

Using the getEndpoints() method with getAllQueueARNs() gives me all of the available Quick Connect Endpoints. Each endpoint contains a type (“queue” or “agent”) and the ARN. When dialing a Quick Connect, I use this code to create the endpoint…

    var endpoint = new connect.Endpoint({
        endpointARN:    <<quickconnect ARN>>,
        type:           <<quickconnect type>>
    });

…and either add a connection to the current call (3rd party call) or create a new contact from the agent…

// add connection to call
contact.addConnection(endpoint,{});

// create a new contact
agent.connect(endpoint,{});

I still haven’t figured out how (or if) I can dial/transfer to a queue or agent without a Quick Connect. However, being able to programmatically dial a Quick Connect is a big improvement to WebAgent as I can now script transfers within WebAgent without the agent being required to know which Quick Connect to use.

Also new in the most recent version of the Streams API is the ability to “rotate through the connected and on hold connections of the contact“ using the contact.toggleActiveConnections() method, and to “conference together the active connections of the conversation“ using the contact.conferenceConnections() method.

I’ve yet to implement these in my code but this will give me the ability to better control the transfer process by adding buttons/triggers within a call script rather than simply instructing the agent. Of course, toggling hold and conferencing is usually handles by the agent in the conversation so this isn’t a priority.

Wrap Up

As I’ve worked through this over the past months, I’m more and more satisfied with my integration with Amazon Connect. I’ve tried to do as much as possible without relying on customization of the Amazon Connect call flows and routing but some customization is needed and there are some really cool things that can be done outside and in support of WebAgent through the Connect console. I’m putting more effort there lately and will update you on some of these features soon.