How to configure Last Agent Routing in Dialogue Studio
This article is an example of a Last Agent Routing flow built in AnywhereNow Dialogue Studio. This scenario describes the setup of a flow which will see if the current caller has spoken to an agent before in a past (limited) period. It will also describe the mechanism and maintenance tips behind is as well as the known limitations.
Preview
Configure
Important
This flow uses the EventBus UCC Enricher, if you are already using that you can remove it from this flow example.
-
Login to your Dialogue Studio environment
-
Open or Create a Tab where you want to add the Last Agent Routing
-
From the menu in the top right, select Import and add the following JSON.
CopyJSON[{"id":"9d1897bf.e51ac8","type":"tab","label":"Last Agent Routing","disabled":false,"info":""},{"id":"8feaa27b.8ff3e","type":"group","z":"9d1897bf.e51ac8","name":"Create Last Agent Memory","style":{"label":true},"nodes":["d8803b35.378348","59308899.4ad098","60157bc3.eafbc4","5436bcda.d26f54","4c4c9009.9fb2e","b22fd6d.27fee28","20215017.b623","52f33320946c9b15","dfef4c798aac6048","681f2e72b3737805","837a7daf6b1ae117"],"x":54,"y":219,"w":1412,"h":162},{"id":"cde2b9be.532788","type":"group","z":"9d1897bf.e51ac8","name":"Route based on Last Agent","style":{"label":true},"nodes":["136cd64f.8aa9ca","39e684fe.3fb4fc","c72e2fdb.81a21","b94ee88f.532b08","f177a31e.3d3f","dca895d1.50ef78","dbf6ce9bb343fbeb","d244506714cdceb6","de70b89478d6f443"],"x":54,"y":419,"w":1292,"h":162},{"id":"d3bbfa2b.946f88","type":"group","z":"9d1897bf.e51ac8","name":"Cleanup LAR Memory Daily","style":{"label":true},"nodes":["d35e88d9.761b88","3a77a675.eb31da","27437db2fde412b2"],"x":474,"y":79,"w":372,"h":122},{"id":"77dbd2a880171c7d","type":"group","z":"9d1897bf.e51ac8","name":"Dialogue UCC Indexer","style":{"label":true},"nodes":["7f043d4f89e7241f","9c61f859325da0fe","6b4f6bfaa32ac343"],"x":54,"y":79,"w":392,"h":122},{"id":"d8803b35.378348","type":"any-red-event-bus","z":"9d1897bf.e51ac8","g":"8feaa27b.8ff3e","name":"","config":"","filtertype":"DialogueParticipantAddedEvent,DialogueQueueEnteredEvent","x":140,"y":320,"wires":[["dfef4c798aac6048"]]},{"id":"59308899.4ad098","type":"switch","z":"9d1897bf.e51ac8","g":"8feaa27b.8ff3e","name":"Split participant added based on role","property":"payload.event.roles[0]","propertyType":"msg","rules":[{"t":"eq","v":"Customer","vt":"str"},{"t":"eq","v":"Agent","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":930,"y":300,"wires":[["60157bc3.eafbc4"],["5436bcda.d26f54"]]},{"id":"60157bc3.eafbc4","type":"function","z":"9d1897bf.e51ac8","g":"8feaa27b.8ff3e","name":"Create Last Agent Record for customer","func":"const { sipUri, dialogueId, timestamp } = msg.payload.event;\nconst sipUser = sipUri.slice(4).split(\"@\")[0];\nconst keyUser = sipUser.replaceAll(\".\",\"_\"); // <-- sanitize dot\n\n// attach keyUser to the global dialogue record\nconst dlgKey = `dlgUcc:${dialogueId}`;\nconst rec = global.get(dlgKey) || {};\nrec.keyUser = keyUser;\nglobal.set(dlgKey, rec);\n\nconst uccName = msg.payload.event.uccName || rec.uccName;\nif (!uccName) return msg; // can't scope LAR without UCC\n\nconst larKey = `lar:${uccName}:${keyUser}`;\nconst lar = flow.get(larKey);\n\nif (!lar) {\n flow.set(larKey, { dialogueId, timestamp, uccName });\n return msg;\n}\n\n// If lar exists but missing agentUri, refresh dialogueId/timestamp/uccName\nif (!lar.agentUri) {\n flow.set(larKey, { ...lar, dialogueId, timestamp, uccName });\n}\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1270,"y":260,"wires":[[]]},{"id":"5436bcda.d26f54","type":"function","z":"9d1897bf.e51ac8","g":"8feaa27b.8ff3e","name":"Store previous agent in Last Agent Record","func":"const { sipUri, displayName, dialogueId, timestamp } = msg.payload.event;\n\nconst dlg = global.get(`dlgUcc:${dialogueId}`);\nconst uccName = msg.payload.event.uccName || dlg?.uccName;\nconst keyUser = dlg?.keyUser;\nif (!uccName || !keyUser) return null;\n\nconst larKey = `lar:${uccName}:${keyUser}`;\nconst customer = flow.get(larKey) || {};\nconst queueName = customer.queueName;\n\nflow.set(larKey, {\n ...customer,\n dialogueId,\n timestamp,\n agentUri: sipUri,\n queueName,\n agentDisplayName: displayName\n});\n\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1280,"y":300,"wires":[[]]},{"id":"136cd64f.8aa9ca","type":"any-red-incoming-call","z":"9d1897bf.e51ac8","g":"cde2b9be.532788","name":"","config":"","filtertype":"audiovideo","x":220,"y":520,"wires":[["c72e2fdb.81a21"]]},{"id":"39e684fe.3fb4fc","type":"any-red-say","z":"9d1897bf.e51ac8","g":"cde2b9be.532788","name":"","text":"Welcome","dataType":"str","saymethod":"Default","voice":"","x":960,"y":500,"wires":[["d244506714cdceb6"]]},{"id":"c72e2fdb.81a21","type":"function","z":"9d1897bf.e51ac8","g":"cde2b9be.532788","name":"Retreive Last Agent","func":"const uccName = msg.uccName;\nconst sipUser = msg.session.sipUri.slice(4).split(\"@\")[0];\nconst keyUser = sipUser.replaceAll(\".\", \"_\");\n\nif (!uccName) {\n msg.lastAgent = null;\n return msg;\n}\n\nconst lar = flow.get(`lar:${uccName}:${keyUser}`);\nmsg.lastAgent = lar || null;\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":470,"y":520,"wires":[["b94ee88f.532b08"]]},{"id":"b94ee88f.532b08","type":"switch","z":"9d1897bf.e51ac8","g":"cde2b9be.532788","name":"Check if a last agent is known","property":"lastAgent.agentUri","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":710,"y":520,"wires":[["39e684fe.3fb4fc"],["f177a31e.3d3f"]]},{"id":"f177a31e.3d3f","type":"any-red-say","z":"9d1897bf.e51ac8","g":"cde2b9be.532788","name":"","text":"\"Welcome back, we will connect you with \" & lastAgent.agentDisplayName","dataType":"jsonata","saymethod":"Default","voice":"","x":1010,"y":540,"wires":[["dbf6ce9bb343fbeb"]]},{"id":"4c4c9009.9fb2e","type":"function","z":"9d1897bf.e51ac8","g":"8feaa27b.8ff3e","name":"Store previous skill in Last Agent Record","func":"const { queueName, dialogueId, timestamp } = msg.payload.event;\n\nconst dlg = global.get(`dlgUcc:${dialogueId}`);\nconst uccName = msg.payload.event.uccName || dlg?.uccName;\nconst keyUser = dlg?.keyUser;\n\nif (!uccName || !keyUser) return null;\n\nconst larKey = `lar:${uccName}:${keyUser}`;\nconst lar = flow.get(larKey) || {};\n\n// Only set queueName if we don't have an agent yet\nif (!lar.agentUri) {\n flow.set(larKey, { ...lar, dialogueId, timestamp, queueName });\n}\n\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1280,"y":340,"wires":[[]]},{"id":"b22fd6d.27fee28","type":"switch","z":"9d1897bf.e51ac8","g":"8feaa27b.8ff3e","name":"Filter out IVR stage","property":"payload.event.queueId","propertyType":"msg","rules":[{"t":"neq","v":"-1","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":870,"y":340,"wires":[["4c4c9009.9fb2e"]]},{"id":"20215017.b623","type":"switch","z":"9d1897bf.e51ac8","g":"8feaa27b.8ff3e","name":"Route events","property":"payload.eventType","propertyType":"msg","rules":[{"t":"eq","v":"DialogueParticipantAddedEvent","vt":"str"},{"t":"eq","v":"DialogueQueueEnteredEvent","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":690,"y":320,"wires":[["59308899.4ad098"],["b22fd6d.27fee28"]]},{"id":"dca895d1.50ef78","type":"comment","z":"9d1897bf.e51ac8","g":"cde2b9be.532788","name":"Change to preferred Skill","info":"","x":1210,"y":460,"wires":[]},{"id":"d35e88d9.761b88","type":"function","z":"9d1897bf.e51ac8","g":"d3bbfa2b.946f88","name":"Cleanup Memory","func":"msg.clear = flow.keys()\n\nfor (let i = 0; i< msg.clear.length;i++) {\nflow.set(msg.clear[i])\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":160,"wires":[[]]},{"id":"3a77a675.eb31da","type":"inject","z":"9d1897bf.e51ac8","g":"d3bbfa2b.946f88","name":"","props":[],"repeat":"","crontab":"00 12 * * *","once":false,"onceDelay":0.1,"topic":"","x":570,"y":160,"wires":[["d35e88d9.761b88"]]},{"id":"dbf6ce9bb343fbeb","type":"any-red-action","z":"9d1897bf.e51ac8","g":"cde2b9be.532788","name":"","sweetName":"Empty action","actionType":"preferredhunt","fromSessionId":"session.id","fromDataType":"msg","toSessionId":"fromSessionId","toDataType":"msg","skill":"lastAgent.queueName","skillDataType":"msg","sip":"","sipDataType":"str","agentSips":"lastAgent.agentUri","agentSipsDataType":"msg","config":"fc4953ed8900b279","questionsfilter":"","questionid":"","x":1240,"y":540,"wires":[]},{"id":"d244506714cdceb6","type":"any-red-action","z":"9d1897bf.e51ac8","g":"cde2b9be.532788","name":"","sweetName":"Empty action","actionType":"enqueue","fromSessionId":"session.id","fromDataType":"msg","toSessionId":"fromSessionId","toDataType":"msg","skill":"Support","skillDataType":"str","sip":"","sipDataType":"str","agentSips":"","agentSipsDataType":"str","config":"fc4953ed8900b279","questionsfilter":"","questionid":"","x":1230,"y":500,"wires":[]},{"id":"7f043d4f89e7241f","type":"any-red-event-bus","z":"9d1897bf.e51ac8","g":"77dbd2a880171c7d","name":"","config":"","filtertype":"DialogueStartedEvent,DialogueEndedEvent","x":140,"y":160,"wires":[["9c61f859325da0fe"]]},{"id":"52f33320946c9b15","type":"switch","z":"9d1897bf.e51ac8","g":"8feaa27b.8ff3e","name":"Filter UCC","property":"payload.event.uccName","propertyType":"msg","rules":[{"t":"eq","v":"ucc-name-example","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":530,"y":320,"wires":[["20215017.b623"]]},{"id":"9c61f859325da0fe","type":"function","z":"9d1897bf.e51ac8","g":"77dbd2a880171c7d","name":"Dialogue UCC Indexer","func":"/**\n * Dialogue UCC Indexer\n * ---------------------\n * Purpose:\n * Maintain a simple global (in-memory) mapping from a dialogueId -> uccName.\n * This allows later events that do NOT contain uccName to be enriched.\n *\n * Expected input shape:\n * msg.payload = {\n * eventType: \"DialogueStartedEvent\" | \"DialogueEndedEvent\" | ...,\n * event: {\n * dialogueId: string,\n * uccName?: string,\n * ...other fields\n * }\n * }\n *\n * Global context keys:\n * dlgUcc:<dialogueId> => { uccName: string }\n *\n * Notes:\n * - This is an in-memory cache. If Node-RED restarts, it resets.\n * - We only clear the mapping on DialogueEndedEvent.\n */\n\nconst payload = msg.payload || {};\nconst ev = payload.event;\nconst type = payload.eventType;\n\n// If the message is not in the expected shape, just pass it through.\nif (!ev || !ev.dialogueId) return msg;\n\nconst dialogueId = ev.dialogueId;\nconst key = `dlgUcc:${dialogueId}`;\n\n// When a dialogue starts, capture the uccName if present.\nif (type === \"DialogueStartedEvent\") {\n if (ev.uccName) {\n // Store only what we need; keep the record extensible for the future.\n const rec = global.get(key) || {};\n rec.uccName = ev.uccName;\n global.set(key, rec);\n }\n return msg;\n}\n\n// When a dialogue ends, remove the cached mapping.\nif (type === \"DialogueEndedEvent\") {\n // Node-RED global context does not have a delete API; setting undefined clears it in practice.\n global.set(key, undefined);\n return msg;\n}\n\n// Optional behavior:\n// If other dialogue-scoped events pass through and happen to contain uccName,\n// we can refresh the cached value.\nif (ev.uccName) {\n const rec = global.get(key) || {};\n rec.uccName = ev.uccName;\n global.set(key, rec);\n}\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":160,"wires":[[]],"icon":"node-red/file-in.svg"},{"id":"dfef4c798aac6048","type":"function","z":"9d1897bf.e51ac8","g":"8feaa27b.8ff3e","name":"Dialogue UCC Enricher","func":"/**\n * Dialogue UCC Enricher\n * --------------------\n * Purpose:\n * Ensure msg.payload.event.uccName is present for dialogue-scoped events.\n *\n * How it works:\n * 1) If the incoming event already has uccName -> do nothing.\n * 2) Otherwise, look up uccName from the global cache created in Stage 1\n * (key: dlgUcc:<dialogueId>).\n * 3) If found, inject it into msg.payload.event (without mutating other fields).\n *\n * Notes:\n * - If the cache is missing (e.g., Node-RED restart, or start event never observed),\n * this node simply passes the message through unchanged.\n */\n\nconst payload = msg.payload || {};\nconst ev = payload.event;\n\n// If the message is not in the expected shape, pass through.\nif (!ev || !ev.dialogueId) return msg;\n\n// If uccName is already present, we don't need to enrich.\nif (ev.uccName) return msg;\n\nconst key = `dlgUcc:${ev.dialogueId}`;\nconst rec = global.get(key);\nconst uccName = rec && rec.uccName;\n\n// If we don't have a cached uccName, pass through unchanged.\nif (!uccName) return msg;\n\n// Create a new payload object so downstream nodes see the enriched value.\nmsg.payload = {\n ...payload,\n event: {\n ...ev,\n uccName\n }\n};\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":320,"wires":[["52f33320946c9b15"]],"icon":"node-red/file-out.svg"},{"id":"6b4f6bfaa32ac343","type":"comment","z":"9d1897bf.e51ac8","g":"77dbd2a880171c7d","name":"TODO: Configure UCC config","info":"","x":200,"y":120,"wires":[]},{"id":"681f2e72b3737805","type":"comment","z":"9d1897bf.e51ac8","g":"8feaa27b.8ff3e","name":"TODO: Configure UCC config","info":"","x":200,"y":280,"wires":[]},{"id":"de70b89478d6f443","type":"comment","z":"9d1897bf.e51ac8","g":"cde2b9be.532788","name":"TODO: Configure UCC config","info":"","x":200,"y":480,"wires":[]},{"id":"837a7daf6b1ae117","type":"comment","z":"9d1897bf.e51ac8","g":"8feaa27b.8ff3e","name":"TODO: Configure UCC name or remove switch for all UCCs","info":"","x":720,"y":260,"wires":[]},{"id":"27437db2fde412b2","type":"comment","z":"9d1897bf.e51ac8","g":"d3bbfa2b.946f88","name":"Optional: Configure clear schedule","info":"","x":640,"y":120,"wires":[]},{"id":"fc4953ed8900b279","type":"any-red-config","active":true,"ucc":"ucc--sales-michael-948lq","priority":"1"},{"id":"e0fb69bff26a7761","type":"global-config","env":[],"modules":{"anywherenow-red":"1.0.0"}}] -
Open the both EventBus nodes and Select or Configure a server. For more information, see: Create your first flow
-
Open the Incoming Call and Select or Configure a server. For more information, see: Create your first flow
-
Open the Enqueue and enter a Skill
-
Alternatively, if you want Last Agent Routing to apply to multiple UCCs, add more options to the switch. To apply it to all UCCs, remove the switch node.
-
To test your Last Agent Routing, initiate a phone call with the UCC and have an Agent accept the conversation. Now call again to see if the Last Agent Routing has been updated.
-
If you need to customize or extend the IVR Interactive Voice Response, or IVR, is a telephone application to take orders via telephone keypad or voice through a computer. By choosing menu options the caller receives information, without the intervention of a human operator, or will be forwarded to the appropriate Agent., you can open the Tab, modify the nodes and responses as needed, and save your changes.
-
Test the IVR again to make sure it works.
-
Congratulations, you have now successfully configured Last Agent Routing in Dialogue Studio!
Explanation
This Dialogue Studio flow triggers on an incoming customer conversation. It attempts to route the caller to the last agent they spoke to within the same UCC (if a previous agent is known). If no last agent is known, the flow routes the call to a standard skill.
Setup EventBus
The flow listens for dialogue-related events using the EventBus nodes. These events are used to build (and maintain) a lightweight in-memory record that links a customer to their last agent and queue.
UCC scoping (optional but recommended)
The flow can scope Last Agent Routing per UCC:
-
Dialogue UCC Indexer caches
dialogueId -> uccNamein global context (key:dlgUcc:<dialogueId>) so later events that do not includeuccNamecan still be enriched. -
Dialogue UCC Enricher injects
uccNameinto dialogue-scoped events when possible. -
Filter UCC (switch node) optionally restricts the LAR record creation to one or more UCCs. If you remove this switch, the flow will create LAR records for all UCCs.
Storing the Last Agent Routing in memory
When a customer joins a dialogue (participant role = Customer), the flow creates or updates an in-memory record keyed by uccName and the caller identity (derived from the SIP The Session Initiation Protocol, or SIP, is a protocol for multimedia communication (audio, video and data communication). SIP is also used for Voice over IP (VoIP). SIP has interactions with other Internet protocols such as HTTP and SMTP. URI). The record stores the current dialogueId and timestamp.
When the customer enters a queue, the flow stores the queue name in the same record (used later as the routing skill).
When an agent joins the dialogue (participant role = Agent), the flow updates the record with the agent’s SIP URI and display name.
Setup the retention policy
This example stores routing memory in the flow context (in-memory). To avoid unbounded growth, a daily scheduled Inject triggers a cleanup function that clears the stored LAR keys in flow context.
Setup Incoming conversation
The flow listens for incoming calls using the Incoming Call node. When a call comes in, it calculates the caller key from the session SIP URI and retrieves the LAR record from flow context (scoped by UCC).
Setup Last Agent Routing logic
If a last agent is available in the stored record, the flow uses a PreferredHunt action to attempt routing to that agent (and uses the stored queue name as the skill for routing context). If no last agent is known, the flow routes the call to the default Enqueue action (standard skill).
In both scenarios, the flow plays a welcome message to the caller using a Say node before routing.
