Customizing Genesys Cloud Routing Strategies with TypeScript
What You Will Build
- Build a TypeScript routing engine that calculates weighted agent scores using interaction context and agent attributes.
- Query the Genesys Cloud Routing API for real-time availability and skill matches.
- Expose a configuration API for dynamic weight updates and simulate routing outcomes against historical conversation data.
Prerequisites
- OAuth Client Credentials flow with
routing:agent:read,routing:queue:read,analytics:conversations:readscopes. @genesyscloud/purecloud-sdkv2.200.0 or higher.- Node.js 18+ and TypeScript 5+.
- Dependencies:
express,axios,dotenv,uuid. - A Genesys Cloud organization with at least one queue, routing profile, and active agents.
Authentication Setup
Genesys Cloud uses OAuth 2.0 for all API access. A server-side routing engine requires the Client Credentials grant type because the routing logic executes without end-user interaction. The following implementation caches the access token and handles automatic refresh before expiration.
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import dotenv from 'dotenv';
dotenv.config();
interface TokenResponse {
access_token: string;
expires_in: number;
token_type: string;
scope: string;
}
class GenesysAuth {
private client: AxiosInstance;
private tokenCache: { token: string; expiry: number } | null = null;
private readonly clientId: string;
private readonly clientSecret: string;
private readonly environment: string;
constructor(clientId: string, clientSecret: string, environment: string = 'mypurecloud.com') {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.environment = environment;
this.client = axios.create({
baseURL: `https://${this.environment}/oauth/token`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
}
private async fetchToken(): Promise<string> {
const params = new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
scope: 'routing:agent:read routing:queue:read analytics:conversations:read'
});
const response = await this.client.post<TokenResponse>('', params);
const expiresInMs = response.data.expires_in * 1000;
this.tokenCache = {
token: response.data.access_token,
expiry: Date.now() + expiresInMs - 60000
};
return response.data.access_token;
}
async getAccessToken(): Promise<string> {
if (this.tokenCache && Date.now() < this.tokenCache.expiry) {
return this.tokenCache.token;
}
return this.fetchToken();
}
}
The token cache subtracts sixty seconds from the actual expiry time to prevent race conditions during high-throughput routing evaluations. The scope string explicitly requests read access to routing agents, queues, and conversation analytics.
Implementation
Step 1: Query Agent Availability and Routing Profiles
The routing engine requires real-time agent states. The Routing API provides availability status and routing profiles through /api/v2/routing/users/{userId}/availability and /api/v2/routing/users. The SDK handles serialization and pagination automatically.
import { PlatformClient, RoutingApi } from '@genesyscloud/purecloud-sdk';
export async function fetchAvailableAgents(
auth: GenesysAuth,
environment: string,
queueId: string
): Promise<any[]> {
const client = new PlatformClient();
await client.loginOAuth({
clientId: process.env.GENESYS_CLIENT_ID!,
clientSecret: process.env.GENESYS_CLIENT_SECRET!,
environment: environment
});
const routingApi = new RoutingApi();
const agents: any[] = [];
let pageToken: string | undefined = undefined;
const pageSize = 25;
do {
try {
const response = await routingApi.postRoutingUsers({
body: {
pageSize,
pageToken,
query: `queueId="${queueId}" AND state="available"`
}
});
if (response.body?.entities) {
agents.push(...response.body.entities);
}
pageToken = response.body?.nextPageToken;
} catch (error: any) {
if (error?.response?.status === 429) {
await handleRateLimit(error);
continue;
}
throw error;
}
} while (pageToken);
return agents;
}
async function handleRateLimit(error: any): Promise<void> {
const retryAfter = error?.response?.headers['retry-after'] || 5;
const backoff = Math.min(retryAfter * 1000, 30000);
await new Promise(resolve => setTimeout(resolve, backoff));
}
The postRoutingUsers endpoint accepts a Lucene-style query string. Filtering by queueId and state="available" reduces payload size before scoring. The retry logic reads the retry-after header and caps backoff at thirty seconds to prevent cascading delays.
Step 2: Implement the Custom Scoring Engine
Custom scoring requires a deterministic algorithm that evaluates multiple dimensions. The following engine accepts configurable weights, applies them to agent attributes, and handles tie-breaking through a least-busy fallback or seeded randomization.
export interface RoutingConfig {
weights: {
skillMatch: number;
availabilityDuration: number;
currentLoad: number;
attributeMatch: number;
};
scoreThreshold: number;
tieBreaker: 'least_busy' | 'random';
seed?: string;
}
export interface AgentScore {
userId: string;
name: string;
score: number;
breakdown: Record<string, number>;
}
export function calculateAgentScores(
agents: any[],
interactionContext: { requiredSkills: string[]; attributes: Record<string, string> },
config: RoutingConfig
): AgentScore[] {
const totalWeight = Object.values(config.weights).reduce((a, b) => a + b, 0);
return agents.map(agent => {
const profile = agent.routingProfile || {};
const availability = agent.availabilityStatus || {};
const skills = profile.skills || [];
const skillMatchScore = calculateSkillMatch(skills, interactionContext.requiredSkills);
const availabilityScore = calculateAvailabilityScore(availability);
const loadScore = calculateLoadScore(agent);
const attributeScore = calculateAttributeMatch(agent, interactionContext.attributes);
const rawScore =
(skillMatchScore * config.weights.skillMatch) +
(availabilityScore * config.weights.availabilityDuration) +
((1 - loadScore) * config.weights.currentLoad) +
(attributeScore * config.weights.attributeMatch);
const normalizedScore = rawScore / totalWeight;
return {
userId: agent.id,
name: agent.name,
score: normalizedScore,
breakdown: {
skillMatch: skillMatchScore,
availability: availabilityScore,
load: 1 - loadScore,
attributes: attributeScore
}
};
}).filter(s => s.score >= config.scoreThreshold);
}
function calculateSkillMatch(agentSkills: any[], required: string[]): number {
if (required.length === 0) return 1;
const matched = required.filter(req => agentSkills.some(s => s.id === req)).length;
return matched / required.length;
}
function calculateAvailabilityScore(status: any): number {
const state = status.stateValue?.toLowerCase();
if (state === 'available') return 1;
if (state === 'away' || state === 'paused') return 0.5;
return 0;
}
function calculateLoadScore(agent: any): number {
const activeConversations = agent.interactionCount || 0;
return Math.min(activeConversations / 5, 1);
}
function calculateAttributeMatch(agent: any, requiredAttrs: Record<string, string>): number {
if (Object.keys(requiredAttrs).length === 0) return 1;
const agentAttrs = agent.attributes || {};
let matched = 0;
for (const key of Object.keys(requiredAttrs)) {
if (agentAttrs[key]?.toLowerCase() === requiredAttrs[key].toLowerCase()) matched++;
}
return matched / Object.keys(requiredAttrs).length;
}
The algorithm normalizes all dimensions to a 0-1 range before applying weights. The loadScore caps at five concurrent interactions to prevent denominator skew. The threshold filter removes candidates below the minimum acceptable quality.
Step 3: Simulate Routing Outcomes with Historical Analytics
Simulation requires historical conversation data. The Analytics API returns interaction details through /api/v2/analytics/conversations/details/query. The following function retrieves past interactions, applies the scoring engine, and generates an efficiency report.
import { AnalyticsApi } from '@genesyscloud/purecloud-sdk';
export async function simulateRouting(
auth: GenesysAuth,
environment: string,
queueId: string,
dateFrom: string,
dateTo: string,
config: RoutingConfig
): Promise<{ matchRate: number; averageScore: number; totalInteractions: number }> {
const client = new PlatformClient();
await client.loginOAuth({
clientId: process.env.GENESYS_CLIENT_ID!,
clientSecret: process.env.GENESYS_CLIENT_SECRET!,
environment: environment
});
const analyticsApi = new AnalyticsApi();
const query = {
dateFrom,
dateTo,
entities: [{ id: queueId, type: 'queue' }],
interval: 'P1D',
metrics: ['conversationsHandled'],
groupBy: ['queueId'],
pageSize: 100,
fields: ['queueId', 'wrapUpCode', 'skillGroup']
};
let totalInteractions = 0;
let matchedInteractions = 0;
let scoreSum = 0;
let pageToken: string | undefined;
do {
try {
const response = await analyticsApi.postAnalyticsConversationsDetailsQuery({ body: query });
const results = response.body?.results || [];
for (const result of results) {
totalInteractions += result.value || 0;
const context = {
requiredSkills: result.dimensions?.skillGroup ? [result.dimensions.skillGroup] : [],
attributes: { wrapUp: result.dimensions?.wrapUpCode || 'unknown' }
};
const agents = await fetchAvailableAgents(auth, environment, queueId);
const scores = calculateAgentScores(agents, context, config);
if (scores.length > 0) {
matchedInteractions++;
scoreSum += scores[0].score;
}
}
pageToken = response.body?.nextPageToken;
if (pageToken) {
query.pageToken = pageToken;
}
} catch (error: any) {
if (error?.response?.status === 429) {
await handleRateLimit(error);
continue;
}
throw error;
}
} while (pageToken);
return {
matchRate: totalInteractions > 0 ? matchedInteractions / totalInteractions : 0,
averageScore: matchedInteractions > 0 ? scoreSum / matchedInteractions : 0,
totalInteractions
};
}
The analytics query groups by queue and skill group to reconstruct historical routing context. The simulation runs the scoring engine against each historical bucket and tracks match rate and average score. Pagination advances via nextPageToken until exhaustion.
Step 4: Expose a Dynamic Configuration API
Dynamic updates require an HTTP endpoint that validates incoming weights, applies them to the routing engine, and returns the active configuration. Express handles the request lifecycle.
import express, { Request, Response, NextFunction } from 'express';
const app = express();
app.use(express.json());
let activeConfig: RoutingConfig = {
weights: { skillMatch: 40, availabilityDuration: 20, currentLoad: 25, attributeMatch: 15 },
scoreThreshold: 0.6,
tieBreaker: 'least_busy'
};
app.post('/api/routing/config', (req: Request, res: Response, next: NextFunction) => {
try {
const { weights, scoreThreshold, tieBreaker } = req.body;
if (!weights || typeof weights !== 'object') {
return res.status(400).json({ error: 'Invalid weights structure' });
}
if (scoreThreshold !== undefined && (typeof scoreThreshold !== 'number' || scoreThreshold < 0 || scoreThreshold > 1)) {
return res.status(400).json({ error: 'scoreThreshold must be between 0 and 1' });
}
if (tieBreaker !== undefined && !['least_busy', 'random'].includes(tieBreaker)) {
return res.status(400).json({ error: 'tieBreaker must be least_busy or random' });
}
activeConfig = {
weights: { ...activeConfig.weights, ...weights },
scoreThreshold: scoreThreshold ?? activeConfig.scoreThreshold,
tieBreaker: tieBreaker ?? activeConfig.tieBreaker
};
res.status(200).json({ message: 'Configuration updated', config: activeConfig });
} catch (error) {
next(error);
}
});
app.get('/api/routing/config', (_req: Request, res: Response) => {
res.status(200).json(activeConfig);
});
The configuration endpoint validates input types and ranges before mutation. It returns the updated configuration immediately. The routing engine reads activeConfig synchronously during scoring, ensuring zero-downtime updates.
Complete Working Example
The following script combines authentication, agent fetching, scoring, simulation, and the configuration API into a single executable module. Replace the environment variables with valid credentials before execution.
import dotenv from 'dotenv';
dotenv.config();
import { GenesysAuth } from './auth';
import { fetchAvailableAgents, calculateAgentScores, RoutingConfig } from './routing';
import { simulateRouting } from './simulation';
import express from 'express';
const ENV = process.env.GENESYS_ENV || 'mypurecloud.com';
const QUEUE_ID = process.env.TARGET_QUEUE_ID!;
const auth = new GenesysAuth(process.env.GENESYS_CLIENT_ID!, process.env.GENESYS_CLIENT_SECRET!, ENV);
let config: RoutingConfig = {
weights: { skillMatch: 40, availabilityDuration: 20, currentLoad: 25, attributeMatch: 15 },
scoreThreshold: 0.6,
tieBreaker: 'least_busy'
};
async function evaluateRouting(interactionContext: { requiredSkills: string[]; attributes: Record<string, string> }) {
const agents = await fetchAvailableAgents(auth, ENV, QUEUE_ID);
const scores = calculateAgentScores(agents, interactionContext, config);
if (scores.length === 0) {
console.log('No agents meet the score threshold.');
return null;
}
scores.sort((a, b) => b.score - a.score);
if (scores[0].score === scores[1]?.score) {
if (config.tieBreaker === 'random') {
const randomIndex = Math.floor(Math.random() * scores.filter(s => s.score === scores[0].score).length);
return scores[randomIndex];
}
return scores[0];
}
return scores[0];
}
const app = express();
app.use(express.json());
app.post('/api/routing/config', (req, res) => {
config = { ...config, ...req.body };
res.json({ status: 'updated', config });
});
app.post('/api/route', async (req, res) => {
try {
const winner = await evaluateRouting(req.body.context);
res.json(winner);
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.get('/api/simulate', async (req, res) => {
try {
const report = await simulateRouting(auth, ENV, QUEUE_ID, req.query.from as string, req.query.to as string, config);
res.json(report);
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Routing engine listening on port ${PORT}`));
The module exports three endpoints. POST /api/route accepts interaction context and returns the highest-scoring agent. POST /api/routing/config updates weights at runtime. GET /api/simulate runs historical analysis. The evaluateRouting function applies tie-breaking logic after sorting by descending score.
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token expired or the client credentials are invalid.
- Fix: Verify
GENESYS_CLIENT_IDandGENESYS_CLIENT_SECRETmatch a valid OAuth client in the Genesys Cloud admin console. Ensure the token cache refreshes before expiry. TheGenesysAuthclass subtracts sixty seconds from the TTL to prevent mid-request failures. - Code: Replace static token storage with the provided
GenesysAuthwrapper. Callauth.getAccessToken()before each SDK initialization.
Error: 429 Too Many Requests
- Cause: The Routing or Analytics API exceeded the organization rate limit. Genesys Cloud enforces per-endpoint and per-tenant quotas.
- Fix: Implement exponential backoff and respect the
retry-afterheader. ThehandleRateLimitfunction parses the header and delays execution. For high-volume routing, cache agent availability for five seconds to reduce query frequency. - Code: Wrap API calls in
try/catchblocks that checkerror?.response?.status === 429and await the backoff timer before retrying the same request.
Error: 403 Forbidden
- Cause: The OAuth client lacks required scopes. Routing queries require
routing:agent:readandrouting:queue:read. Analytics simulation requiresanalytics:conversations:read. - Fix: Navigate to the Genesys Cloud admin console, select the OAuth client, and add the missing scopes. Regenerate the client secret if the client was recently modified.
- Code: Update the
scopeparameter inGenesysAuth.fetchToken()to include all required permissions. Verify the token response contains the exact scope strings.
Error: SDK Pagination Infinite Loop
- Cause: The
nextPageTokenreturns an empty string instead ofundefined, causing the loop to continue with no new data. - Fix: Check for
pageTokentruthiness and reset the query object correctly. The SDK returnsundefinedwhen no further pages exist. - Code: Use
pageToken = response.body?.nextPageToken || undefined;and break the loop ifagentslength does not increase after two consecutive iterations.