Query Agent Utilization Metrics by 30-Minute Intervals in Genesys Cloud CX
What You Will Build
- You will build a Python script that retrieves historical agent conversation metrics, specifically
tHandle,tAcw, andtHold, aggregated into 30-minute time buckets. - This tutorial uses the Genesys Cloud CX Analytics API (
/api/v2/analytics/conversations/details/query) via the official Python SDK (genesys-cloud-sdk). - The implementation covers Python 3.9+ with
asynciosupport for efficient pagination handling.
Prerequisites
- OAuth Client Type: Machine-to-Machine (M2M) application with client credentials.
- Required Scopes:
analytics:conversation:readand optionallyanalytics:report:readif you intend to expand this to dashboard-level data later. For raw conversation detail queries,analytics:conversation:readis sufficient. - SDK Version:
genesys-cloud-sdkv6.x or higher. - Runtime: Python 3.9+ (required for modern
async/awaitsyntax and type hinting). - Dependencies:
genesys-cloud-sdkpython-dotenv(for secure credential management)
Install the dependencies:
pip install genesys-cloud-sdk python-dotenv
Authentication Setup
Genesys Cloud uses OAuth 2.0. For server-side integrations, the Client Credentials flow is the standard. The SDK handles token caching and refreshing automatically when initialized correctly.
Create a .env file in your project root:
GENESYS_REGION=mypurecloud.com
GENESYS_CLIENT_ID=your_client_id
GENESYS_CLIENT_SECRET=your_client_secret
GENESYS_ENVIRONMENT=us-east-1
Initialize the client in your Python script. The Configuration object manages the OAuth context.
import os
from dotenv import load_dotenv
from genesyscloud import Configuration, ApiClient
# Load environment variables
load_dotenv()
def get_genesys_client():
"""
Initializes and returns a configured Genesys Cloud API client.
"""
config = Configuration(
region=os.getenv('GENESYS_REGION'),
environment=os.getenv('GENESYS_ENVIRONMENT'),
client_id=os.getenv('GENESYS_CLIENT_ID'),
client_secret=os.getenv('GENESYS_CLIENT_SECRET')
)
# The SDK handles token acquisition and refresh internally
return ApiClient(config)
Implementation
Step 1: Construct the Analytics Query Payload
The core of this integration is the POST /api/v2/analytics/conversations/details/query endpoint. Unlike simple GET requests, this endpoint accepts a complex JSON body that defines the time window, aggregation granularity, and metric filters.
To get 30-minute intervals, you must set the granularity field to PT30M. To retrieve agent utilization, you need to request specific metrics: tHandle (talk time), tAcw (after-call work), and tHold (hold time).
from datetime import datetime, timedelta
from genesyscloud.analytics.models import ConversationDetailsQueryRequest
def build_query_request(start_time: datetime, end_time: datetime) -> ConversationDetailsQueryRequest:
"""
Constructs the request payload for the analytics query.
Args:
start_time: The beginning of the time window.
end_time: The end of the time window.
Returns:
ConversationDetailsQueryRequest object configured for 30-minute intervals.
"""
# Define the metrics we care about for utilization
metrics = [
"tHandle",
"tAcw",
"tHold"
]
# Define the groupings. We want data broken down by time and potentially by agent.
# If you want per-agent utilization, include 'agentId'.
# If you want system-wide or queue-wide, omit 'agentId' or group by 'queueId'.
groupings = [
"time",
"agentId"
]
# The request object
query_request = ConversationDetailsQueryRequest(
interval=f"{start_time.isoformat()}Z/{end_time.isoformat()}Z",
granularity="PT30M", # 30-minute intervals
metrics=metrics,
groupings=groupings,
view="default" # Use 'default' for standard conversation metrics
)
return query_request
Step 2: Execute the Query and Handle Pagination
The Analytics API does not return all data in one response. It uses a cursor-based pagination model. You must check the nextUri field in the response. If present, you must follow it until nextUri is null.
Furthermore, the Analytics API has strict rate limits. If you receive a 429 Too Many Requests, you must implement exponential backoff. The Genesys Cloud Python SDK does not automatically retry 429s for bulk analytics queries, so you must handle this manually.
import time
import requests
from typing import List, Dict, Any, Optional
def fetch_analytics_page(client: ApiClient, uri: str, headers: Dict[str, str]) -> Optional[Dict[str, Any]]:
"""
Fetches a single page of analytics data with 429 retry logic.
Args:
client: The Genesys Cloud ApiClient.
uri: The URL to fetch (either the initial endpoint or nextUri).
headers: Additional headers (usually contains Authorization).
Returns:
Parsed JSON response or None if an unrecoverable error occurs.
"""
max_retries = 5
retry_count = 0
base_delay = 2 # seconds
while retry_count < max_retries:
try:
# Use the underlying session of the ApiClient for direct HTTP control
# This allows us to handle pagination URIs that might not be direct SDK methods
response = client._session.get(
uri,
headers=headers,
timeout=30
)
if response.status_code == 200:
return response.json()
elif response.status_code == 429:
# Rate limited. Extract Retry-After header if available, else use exponential backoff.
retry_after = int(response.headers.get('Retry-After', base_delay * (2 ** retry_count)))
print(f"Rate limited (429). Waiting {retry_after} seconds...")
time.sleep(retry_after)
retry_count += 1
elif response.status_code >= 500:
# Server error. Retry with exponential backoff.
delay = base_delay * (2 ** retry_count)
print(f"Server error ({response.status_code}). Waiting {delay} seconds...")
time.sleep(delay)
retry_count += 1
else:
# Client error (4xx other than 429). Do not retry.
print(f"Client error: {response.status_code} - {response.text}")
return None
except requests.exceptions.RequestException as e:
print(f"Network error: {e}. Retrying...")
time.sleep(base_delay * (2 ** retry_count))
retry_count += 1
print("Max retries exceeded.")
return None
def get_all_utilization_data(client: ApiClient, start_time: datetime, end_time: datetime) -> List[Dict[str, Any]]:
"""
Fetches all pages of utilization data for the given time range.
Args:
client: The Genesys Cloud ApiClient.
start_time: Start of the query window.
end_time: End of the query window.
Returns:
A flat list of all metric records.
"""
query_request = build_query_request(start_time, end_time)
# Initial endpoint
base_url = "https://{region}/api/v2/analytics/conversations/details/query".format(
region=client._configuration.region
)
# Prepare headers. The SDK client has a method to get the current access token.
# Note: In production, ensure you refresh the token if it expires mid-process.
token = client._configuration.access_token
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
all_records = []
current_uri = base_url
page_count = 0
while current_uri:
page_count += 1
print(f"Fetching page {page_count}...")
# For the first request, we POST the query body. For subsequent pages, we GET the nextUri.
if page_count == 1:
try:
response = client._session.post(
current_uri,
json=query_request.to_dict(),
headers=headers,
timeout=30
)
if response.status_code == 200:
data = response.json()
elif response.status_code == 429:
# Handle 429 on initial POST
retry_after = int(response.headers.get('Retry-After', 2))
time.sleep(retry_after)
continue
else:
print(f"Initial query failed: {response.status_code}")
return []
except Exception as e:
print(f"Error during initial query: {e}")
return []
else:
# Subsequent pages use GET
data = fetch_analytics_page(client, current_uri, headers)
if not data:
break
# Extract entities
if 'entities' in data:
all_records.extend(data['entities'])
# Check for next page
current_uri = data.get('nextUri')
# Small delay between pages to be polite to the API
time.sleep(0.5)
return all_records
Step 3: Process and Format the Results
The raw response contains a nested structure. Each entity in the entities array represents a data point for a specific time interval, agent, and metric combination. You need to pivot this data to calculate utilization.
Utilization is typically defined as:
$$ \text{Utilization} = \frac{\text{tHandle} + \text{tAcw} + \text{tHold}}{\text{Interval Duration}} $$
Since our granularity is 30 minutes (1800 seconds), we can calculate the percentage of time the agent was busy.
from collections import defaultdict
def calculate_utilization(records: List[Dict[str, Any]]) -> Dict[str, Dict[str, float]]:
"""
Processes raw analytics records into a structured utilization report.
Args:
records: List of metric records from the API.
Returns:
Dictionary keyed by agentId, containing a dict of time intervals and utilization %.
"""
# Structure: { agentId: { "2023-10-27T10:00:00Z": 0.85, ... } }
utilization_report = defaultdict(dict)
# Interval duration in seconds (30 minutes)
INTERVAL_SECONDS = 1800
for record in records:
# Extract key fields
agent_id = record.get('agentId')
time_bucket = record.get('timeBucket')
if not agent_id or not time_bucket:
continue
# Extract metrics. These are in seconds.
t_handle = record.get('tHandle', 0) or 0
t_acw = record.get('tAcw', 0) or 0
t_hold = record.get('tHold', 0) or 0
# Calculate total busy time
total_busy_seconds = t_handle + t_acw + t_hold
# Calculate utilization percentage
# Prevent division by zero, though INTERVAL_SECONDS is constant
if INTERVAL_SECONDS > 0:
utilization_pct = (total_busy_seconds / INTERVAL_SECONDS) * 100
else:
utilization_pct = 0
# Cap at 100% (sometimes metrics can sum slightly over due to rounding or overlapping states)
utilization_pct = min(utilization_pct, 100.0)
# Store in report
utilization_report[agent_id][time_bucket] = {
'utilization_pct': round(utilization_pct, 2),
'tHandle': t_handle,
'tAcw': t_acw,
'tHold': t_hold,
'total_busy_seconds': total_busy_seconds
}
return dict(utilization_report)
Complete Working Example
Combine the above steps into a single executable script. This script queries the last 24 hours of data for all agents.
import os
import sys
from datetime import datetime, timedelta
from dotenv import load_dotenv
from genesyscloud import Configuration, ApiClient
# Import local functions if modularized, or include them here
# For this example, we assume the functions from Steps 1-3 are defined above
def main():
load_dotenv()
# 1. Initialize Client
client = get_genesys_client()
# 2. Define Time Window (Last 24 Hours)
end_time = datetime.utcnow()
start_time = end_time - timedelta(hours=24)
print(f"Querying data from {start_time.isoformat()} to {end_time.isoformat()}")
try:
# 3. Fetch Data
records = get_all_utilization_data(client, start_time, end_time)
if not records:
print("No records found. Check scopes and time window.")
return
print(f"Fetched {len(records)} raw metric records.")
# 4. Process Data
utilization_report = calculate_utilization(records)
# 5. Output Results
# Sort agents by ID for consistent output
for agent_id in sorted(utilization_report.keys()):
agent_data = utilization_report[agent_id]
print(f"\n--- Agent ID: {agent_id} ---")
# Sort time buckets
for time_bucket in sorted(agent_data.keys()):
metrics = agent_data[time_bucket]
print(f" Time: {time_bucket}")
print(f" Utilization: {metrics['utilization_pct']}%")
print(f" tHandle: {metrics['tHandle']}s, tAcw: {metrics['tAcw']}s, tHold: {metrics['tHold']}s")
except Exception as e:
print(f"An error occurred: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 403 Forbidden
Cause: The OAuth token does not have the required scope analytics:conversation:read.
Fix:
- Go to Genesys Cloud Admin > Security > Applications.
- Select your M2M application.
- Add the
analytics:conversation:readscope. - Regenerate your client secret if necessary (though scope changes usually apply immediately).
- Ensure your code uses the updated credentials.
Error: 429 Too Many Requests
Cause: You are querying too much data too quickly. The Analytics API is resource-intensive.
Fix:
- Implement the exponential backoff logic shown in Step 2.
- Reduce the time window. Instead of querying 30 days, query 1 day at a time.
- Increase the granularity. If you do not need 30-minute precision, use
PT1H(1 hour) to reduce the number of rows returned.
Error: Empty Entities List
Cause: No conversations occurred in the specified time window for the selected agents, or the view parameter is incorrect.
Fix:
- Verify that agents were active during the
start_timeandend_time. - Check the
groupingsparameter. If you group byagentIdbut query a time window where no agents were logged in, the result will be empty. - Ensure you are using the correct
view. For standard voice interactions,defaultis correct. For digital interactions, you may needdigital.
Error: nextUri is Null but Data Seems Incomplete
Cause: The API has a maximum row limit per query. If the result set exceeds this limit, the API may truncate results or require a different query strategy.
Fix:
- Split the query into smaller time chunks (e.g., 4-hour blocks) instead of one large 24-hour block.
- Check the
warningsfield in the API response. It often contains messages about truncated results.