Creating Genesys Cloud Follow-Up Tasks via Task Management API with Python
What You Will Build
- You will build a Python module that creates follow-up tasks in Genesys Cloud using atomic HTTP POST operations to the Task Management API, triggered by Agent Assist context events.
- The code uses the Genesys Cloud REST API (
/api/v2/taskmanagement/queues/{queueId}/tasks) with explicit payload construction, schema validation, duplicate prevention, and webhook synchronization. - This tutorial covers Python 3.9+ using the
requestslibrary with type hints, production-ready error handling, and observability hooks.
Prerequisites
- OAuth Client Credentials flow with
taskmanagement:task:write,routing:queue:read,user:read, androuting:user:readscopes. - Genesys Cloud API v2.
- Python 3.9+ runtime.
- External dependencies:
pip install requests python-dotenv
Authentication Setup
Genesys Cloud uses OAuth 2.0 Client Credentials for server-to-server integrations. The token must be cached and refreshed before expiration to prevent 401 failures during task creation pipelines.
import requests
import time
import json
from typing import Optional
from dataclasses import dataclass
@dataclass
class TokenCache:
access_token: str
expires_at: float
class GenesysAuth:
def __init__(self, base_url: str, client_id: str, client_secret: str):
self.base_url = base_url.rstrip("/")
self.client_id = client_id
self.client_secret = client_secret
self._cache: Optional[TokenCache] = None
def get_token(self) -> str:
if self._cache and time.time() < self._cache.expires_at - 60:
return self._cache.access_token
url = f"{self.base_url}/oauth/token"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "taskmanagement:task:write routing:queue:read user:read routing:user:read"
}
response = requests.post(url, headers=headers, data=data)
response.raise_for_status()
payload = response.json()
self._cache = TokenCache(
access_token=payload["access_token"],
expires_at=time.time() + payload["expires_in"]
)
return self._cache.access_token
Implementation
Step 1: Assignee Validity Verification and Queue Capacity Check
Before creating a task, you must verify that the target assignee exists, is active, and is in an available routing status. Genesys Cloud rejects tasks routed to inactive or offline users. You also validate queue routing configuration to prevent workload constraint violations.
class TaskValidator:
def __init__(self, auth: GenesysAuth):
self.auth = auth
self.session = requests.Session()
def _make_request(self, method: str, path: str, **kwargs) -> requests.Response:
url = f"{self.auth.base_url}{path}"
headers = {"Authorization": f"Bearer {self.auth.get_token()}", "Content-Type": "application/json"}
kwargs.setdefault("headers", headers)
return self.session.request(method, url, **kwargs)
def validate_assignee(self, user_id: str) -> bool:
"""Verify user exists and is available for routing."""
user_resp = self._make_request("GET", f"/api/v2/users/{user_id}")
if user_resp.status_code == 404:
return False
status_resp = self._make_request("GET", f"/api/v2/users/{user_id}/routing/status")
if status_resp.status_code != 200:
return False
status_data = status_resp.json()
return status_data.get("routingStatus") == "available"
def validate_queue_capacity(self, queue_id: str) -> bool:
"""Check if queue is enabled and accepting tasks."""
queue_resp = self._make_request("GET", f"/api/v2/routing/queues/{queue_id}")
if queue_resp.status_code != 200:
return False
queue_data = queue_resp.json()
return queue_data.get("enabled", False) is True
Step 2: Payload Construction with Priority, Due Date, and Custom Attributes
Genesys Cloud Task Management expects specific schema fields. The task-ref maps to externalId, detail-matrix maps to customData, and priority ranges from 1 (highest) to 500 (lowest). Due dates must be in ISO 8601 format and cannot exceed configured SLA windows.
from datetime import datetime, timedelta
from typing import Any, Dict
def build_task_payload(
external_id: str,
queue_id: str,
assignee_id: str,
priority_score: int,
due_date: datetime,
detail_matrix: Dict[str, Any],
max_duration_hours: int = 24
) -> Dict[str, Any]:
"""Construct validated task creation payload."""
# Priority normalization: Genesys accepts 1-500
priority = max(1, min(500, priority_score))
# Due date validation against max duration
creation_time = datetime.utcnow()
max_due = creation_time + timedelta(hours=max_duration_hours)
if due_date > max_due:
due_date = max_due
# Detail matrix flattening for customData
custom_data = {k: str(v) for k, v in detail_matrix.items()}
payload = {
"externalId": external_id,
"routingData": {
"queueId": queue_id,
"assigneeId": assignee_id,
"priority": priority,
"dueDate": due_date.isoformat() + "Z"
},
"customData": custom_data,
"type": "task",
"source": {
"type": "api",
"id": "agent-assist-trigger"
}
}
return payload
Step 3: Duplicate Checking, Atomic Creation, and Retry Logic
You must prevent duplicate tasks by searching existing tasks using the externalId. The creation operation uses exponential backoff for 429 responses and validates the HTTP 201 response. Pagination is handled for the search endpoint.
import logging
import uuid
from typing import Tuple
logger = logging.getLogger("genesys_task_creator")
class TaskCreator:
def __init__(self, auth: GenesysAuth, validator: TaskValidator):
self.auth = auth
self.validator = validator
self.session = requests.Session()
def _check_duplicate(self, external_id: str, queue_id: str) -> bool:
"""Search for existing task by externalId. Returns True if exists."""
url = f"{self.auth.base_url}/api/v2/taskmanagement/tasks/search"
headers = {
"Authorization": f"Bearer {self.auth.get_token()}",
"Content-Type": "application/json"
}
query = {
"query": f'externalId == "{external_id}" AND routingData.queueId == "{queue_id}"',
"pageSize": 20
}
while query.get("pageSize"):
resp = self.session.post(url, headers=headers, json=query)
resp.raise_for_status()
data = resp.json()
if data.get("entities") and len(data["entities"]) > 0:
return True
if data.get("nextPage"):
query = {"nextPage": data["nextPage"], "pageSize": 20}
else:
break
return False
def create_task(
self,
external_id: str,
queue_id: str,
assignee_id: str,
priority_score: int,
due_date: datetime,
detail_matrix: Dict[str, Any],
max_retries: int = 3
) -> Tuple[bool, Dict[str, Any]]:
"""Atomic task creation with validation, duplicate check, and retry logic."""
# Validation pipeline
if not self.validator.validate_assignee(assignee_id):
logger.warning("Assignee %s is not available", assignee_id)
return False, {"error": "assignee_unavailable"}
if not self.validator.validate_queue_capacity(queue_id):
logger.warning("Queue %s is not accepting tasks", queue_id)
return False, {"error": "queue_disabled"}
if self._check_duplicate(external_id, queue_id):
logger.info("Duplicate task detected: %s", external_id)
return False, {"error": "duplicate_task"}
payload = build_task_payload(
external_id, queue_id, assignee_id, priority_score, due_date, detail_matrix
)
url = f"{self.auth.base_url}/api/v2/taskmanagement/queues/{queue_id}/tasks"
headers = {
"Authorization": f"Bearer {self.auth.get_token()}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4())
}
for attempt in range(max_retries + 1):
start_time = time.time()
try:
resp = self.session.post(url, headers=headers, json=payload)
latency = time.time() - start_time
if resp.status_code == 201:
logger.info("Task created successfully in %.3fs", latency)
return True, resp.json()
elif resp.status_code == 429:
wait_time = min(2 ** attempt, 30)
logger.warning("Rate limited. Retrying in %ds", wait_time)
time.sleep(wait_time)
continue
elif resp.status_code == 409:
return False, {"error": "duplicate_task_conflict"}
elif resp.status_code == 422:
return False, {"error": "validation_failed", "details": resp.text}
else:
resp.raise_for_status()
except requests.exceptions.RequestException as e:
logger.error("HTTP request failed: %s", str(e))
return False, {"error": "network_failure", "details": str(e)}
return False, {"error": "max_retries_exceeded"}
Step 4: Webhook Synchronization, Audit Logging, and Metrics Tracking
After creation, you synchronize with an external task manager via outbound webhook, record audit logs, and track latency and success rates for operational governance.
class TaskOrchestrator:
def __init__(self, creator: TaskCreator, webhook_url: str):
self.creator = creator
self.webhook_url = webhook_url
self.success_count = 0
self.failure_count = 0
self.total_latency = 0.0
def _sync_external_manager(self, task_data: Dict[str, Any], status: str) -> None:
"""Push task event to external manager via webhook."""
payload = {
"eventType": "task.created" if status == "success" else "task.failed",
"timestamp": datetime.utcnow().isoformat() + "Z",
"taskId": task_data.get("id", "unknown"),
"externalId": task_data.get("externalId", "unknown"),
"status": status
}
try:
requests.post(self.webhook_url, json=payload, timeout=5)
except requests.exceptions.RequestException:
logger.warning("Webhook sync failed for task %s", payload["taskId"])
def _log_audit(self, external_id: str, result: bool, latency: float, details: Dict) -> None:
"""Write structured audit log for governance."""
audit_entry = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"action": "create_follow_up_task",
"externalId": external_id,
"success": result,
"latency_ms": round(latency * 1000, 2),
"details": details
}
logger.info("AUDIT: %s", json.dumps(audit_entry))
def execute_workflow(
self,
external_id: str,
queue_id: str,
assignee_id: str,
priority_score: int,
due_date: datetime,
detail_matrix: Dict[str, Any]
) -> Dict[str, Any]:
start = time.time()
success, result = self.creator.create_task(
external_id, queue_id, assignee_id, priority_score, due_date, detail_matrix
)
latency = time.time() - start
if success:
self.success_count += 1
self._sync_external_manager(result, "success")
self._log_audit(external_id, True, latency, {"taskId": result.get("id")})
else:
self.failure_count += 1
self._sync_external_manager({"externalId": external_id}, "failed")
self._log_audit(external_id, False, latency, result)
self.total_latency += latency
return {"success": success, "result": result, "latency": latency}
Complete Working Example
The following script combines authentication, validation, creation, and observability into a single executable module. Replace the placeholder credentials with your Genesys Cloud tenant values.
import os
import sys
from datetime import datetime, timedelta
def main():
# Configuration
BASE_URL = os.getenv("GENESYS_BASE_URL", "https://api.mypurecloud.com")
CLIENT_ID = os.getenv("GENESYS_CLIENT_ID")
CLIENT_SECRET = os.getenv("GENESYS_CLIENT_SECRET")
WEBHOOK_URL = os.getenv("EXTERNAL_WEBHOOK_URL", "https://hooks.example.com/genesys-sync")
if not CLIENT_ID or not CLIENT_SECRET:
print("ERROR: GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set")
sys.exit(1)
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
# Initialize components
auth = GenesysAuth(BASE_URL, CLIENT_ID, CLIENT_SECRET)
validator = TaskValidator(auth)
creator = TaskCreator(auth, validator)
orchestrator = TaskOrchestrator(creator, WEBHOOK_URL)
# Task parameters
external_id = f"AA-FOLLOWUP-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
queue_id = "your-queue-id-here"
assignee_id = "your-assignee-user-id-here"
priority_score = 150
due_date = datetime.utcnow() + timedelta(hours=4)
detail_matrix = {
"conversationId": "conv-8821a",
"agentAssistPromptId": "prompt-knowledge-12",
"customerSegment": "enterprise",
"resolutionCategory": "billing-dispute"
}
# Execute creation pipeline
outcome = orchestrator.execute_workflow(
external_id=external_id,
queue_id=queue_id,
assignee_id=assignee_id,
priority_score=priority_score,
due_date=due_date,
detail_matrix=detail_matrix
)
print(json.dumps(outcome, indent=2, default=str))
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token expired or was never generated. The
get_tokenmethod caches tokens but does not automatically refresh during long-running batches. - Fix: Ensure
auth.get_token()is called before each API call. The provided implementation checks expiration with a 60-second safety buffer. If you run batch jobs, add a manual refresh trigger every 10 minutes.
Error: 403 Forbidden
- Cause: The OAuth client lacks required scopes. Task creation requires
taskmanagement:task:write. Assignee validation requiresuser:readandrouting:user:read. - Fix: Update the client credentials application in the Genesys Cloud admin console. Add the missing scopes and regenerate the client secret if the client was created before scope updates.
Error: 409 Conflict
- Cause: A task with the same
externalIdalready exists in the target queue. Genesys Cloud enforces uniqueness per queue for external identifiers. - Fix: The
_check_duplicatemethod queries the search API before posting. If you receive 409 despite the check, another process created the task concurrently. Implement database-level locking or use theIdempotency-Keyheader consistently.
Error: 422 Unprocessable Entity
- Cause: Invalid payload schema. Common triggers include
priorityoutside 1-500,dueDatein the past, orassigneeIdmismatching the queue member list. - Fix: The
build_task_payloadfunction clamps priority and caps due dates. Verify that the assignee is added to the queue member list in Genesys Cloud routing settings.
Error: 429 Too Many Requests
- Cause: Exceeded Genesys Cloud API rate limits (typically 20 requests per second per client for task endpoints).
- Fix: The
create_taskmethod implements exponential backoff with a 30-second maximum wait. For high-volume pipelines, implement a token bucket rate limiter before calling the API.