Debug EventBridge Rule Patterns for Genesys Cloud Conversation Events
What You Will Build
- A Python utility that validates AWS EventBridge event patterns against real Genesys Cloud conversation lifecycle events.
- The code uses the Genesys Cloud Python SDK to simulate event payloads and the
boto3library to test pattern matching logic locally. - The tutorial covers Python.
Prerequisites
- AWS Account: Access to Amazon EventBridge and permissions to create/test rules.
- Genesys Cloud Account: Developer access to a Genesys Cloud organization.
- OAuth Credentials: A Genesys Cloud OAuth Client ID and Secret with the
admin:api:writescope (to retrieve user data for simulation) orconversation:viewscope. - Python Environment: Python 3.8+ installed.
- Dependencies:
pip install purecloud-platform-client-v2 boto3 jsonschema
Authentication Setup
To debug event patterns effectively, you need realistic payload data. The Genesys Cloud EventBridge integration sends specific JSON structures. You cannot rely on generic placeholder data because the detail object structure varies significantly between conversation.created, conversation.updated, and conversation.ended events.
First, authenticate with Genesys Cloud to fetch real user and queue data. This ensures your simulation matches the exact schema Genesys Cloud emits.
import os
import json
from purecloud_platform_client_v2 import PlatformClientBuilder, ConversationApi, UserApi, QueueApi
from purecloud_platform_client_v2.rest import ApiException
def get_genesys_client():
"""
Initialize the Genesys Cloud Platform Client.
Ensure GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET are set in environment.
"""
builder = PlatformClientBuilder()
builder.set_client_id(os.getenv("GENESYS_CLIENT_ID"))
builder.set_client_secret(os.getenv("GENESYS_CLIENT_SECRET"))
builder.set_base_url("https://api.mypurecloud.com") # Adjust region if necessary
try:
client = builder.build()
client.login()
return client
except ApiException as e:
print(f"Authentication failed: {e.status} - {e.reason}")
raise SystemExit("Check your OAuth credentials.")
def fetch_sample_data(client: PlatformClientBuilder):
"""
Fetch a real user and queue to construct a realistic event payload.
"""
user_api = client.ConversationApi() # Note: UserApi is separate, but we need conversation context
# Actually, let's get a user first to use as the initiator
user_api_inst = client.UserApi()
queue_api_inst = client.QueueApi()
try:
# Get the first user
users = user_api_inst.get_users(page_size=1)
if not users.entities:
raise Exception("No users found in organization.")
sample_user = users.entities[0]
# Get the first queue
queues = queue_api_inst.get_queues(page_size=1)
if not queues.entities:
raise Exception("No queues found in organization.")
sample_queue = queues.entities[0]
return sample_user, sample_queue
except ApiException as e:
print(f"Failed to fetch sample data: {e.status}")
raise
# Initialize client
genesys_client = get_genesys_client()
sample_user, sample_queue = fetch_sample_data(genesys_client)
Implementation
Step 1: Construct the Genesys Cloud EventBridge Payload
Genesys Cloud sends events to EventBridge in a specific format. The top-level keys are id, source, account, time, region, detail-type, and detail. The source is always genesys.cloud. The account is your AWS Account ID. The detail contains the actual Genesys Cloud resource data.
A common mistake is assuming the detail object matches the REST API response exactly. It often contains a subset of fields or nested structures specific to the event type.
Below is a function that constructs a realistic conversation.created event payload using the real data fetched in the previous step.
import datetime
import uuid
def construct_conversation_created_event(user, queue, aws_account_id: str):
"""
Constructs a realistic Genesys Cloud 'conversation.created' EventBridge payload.
Args:
user: PureCloudPlatformClientV2.User object
queue: PureCloudPlatformClientV2.Queue object
aws_account_id: Your AWS Account ID (string)
Returns:
dict: A valid EventBridge event payload
"""
# Generate a fake conversation ID matching Genesys UUID format
conv_id = str(uuid.uuid4())
# The timestamp must be in ISO 8601 format
current_time = datetime.datetime.utcnow().isoformat() + "Z"
# The payload structure
event_payload = {
"id": str(uuid.uuid4()),
"source": "genesys.cloud",
"account": aws_account_id,
"time": current_time,
"region": "us-east-1", # Genesys Cloud events typically originate from us-east-1 or the specific region
"detail-type": "conversation.created",
"resources": [
f"arn:aws:genesyscloud:{aws_account_id}:conversation:{conv_id}"
],
"detail": {
"id": conv_id,
"type": "voice",
"state": "initiated",
"direction": "inbound",
"priority": 0,
"origin": {
"type": "phone",
"phone": {
"phoneNumber": "+15551234567"
}
},
"wrapupRequired": False,
"routing": {
"skillRequirements": {},
"queue": {
"id": queue.id,
"name": queue.name
},
"skillGroup": None,
"media": "voice",
"skillAssignments": []
},
"participants": [
{
"id": str(uuid.uuid4()),
"name": user.name,
"type": "customer",
"address": {
"type": "phone",
"phone": {
"phoneNumber": "+15551234567"
}
},
"state": "connected",
"stateChangedTimestamp": current_time
}
],
"createdTimestamp": current_time,
"updatedTimestamp": current_time
}
}
return event_payload
Step 2: Define the EventBridge Rule Pattern
The core of the debugging process is defining the EventBridge rule pattern. This pattern is a JSON object that filters events. If the pattern does not match the incoming event, the rule does not fire.
Common errors include:
- Case Sensitivity:
Detail-Typevsdetail-type. - Nested Object Mismatch: Expecting
detail.routing.queue.idto be a string when it might be null in some edge cases. - Wildcard Misuse: Using
*when you need a specific value, or vice versa.
Here is a sample pattern that targets voice conversations initiated into a specific queue.
def get_target_rule_pattern(queue_id: str):
"""
Defines an EventBridge rule pattern for a specific queue.
Args:
queue_id: The ID of the Genesys Cloud queue to filter on.
Returns:
dict: The EventBridge rule pattern
"""
pattern = {
"source": [
"genesys.cloud"
],
"detail-type": [
"conversation.created"
],
"detail": {
"type": [
"voice"
],
"routing": {
"queue": {
"id": [
queue_id
]
}
}
}
}
return pattern
Step 3: Validate the Pattern Locally
Before deploying the rule to EventBridge, you can validate the pattern against your simulated payload. AWS EventBridge uses a specific matching algorithm. You can replicate this logic in Python using the jsonschema library or a custom matcher. However, for precise EventBridge behavior, a recursive dictionary matcher is more accurate than standard JSON Schema validation because EventBridge supports array wildcards and exact matches differently.
Below is a robust pattern matcher that mimics EventBridge’s evaluation logic.
def matches_event_pattern(event: dict, pattern: dict) -> bool:
"""
Recursively checks if an event matches a given EventBridge pattern.
Args:
event: The full EventBridge event payload.
pattern: The rule pattern to match against.
Returns:
bool: True if the event matches the pattern, False otherwise.
"""
# Base case: If pattern is empty or wildcard, it matches
if not pattern or pattern == "*":
return True
# If pattern is a list, the event value must be in the list
if isinstance(pattern, list):
return event in pattern
# If pattern is a dictionary, check all keys
if isinstance(pattern, dict):
# If the event value is not a dict, it cannot match a dict pattern
if not isinstance(event, dict):
return False
# Check every key in the pattern
for key, pattern_value in pattern.items():
# If the key is not in the event, it does not match (unless it's a wildcard)
if key not in event:
# Exception: If the pattern value is a wildcard, it matches any value
if pattern_value != "*":
return False
# Recursively check the value
if not matches_event_pattern(event[key], pattern_value):
return False
return True
# If pattern is a primitive (string, number), check for exact match
return event == pattern
# Test the matcher
aws_account_id = os.getenv("AWS_ACCOUNT_ID", "123456789012") # Replace with your AWS Account ID
test_event = construct_conversation_created_event(sample_user, sample_queue, aws_account_id)
test_pattern = get_target_rule_pattern(sample_queue.id)
is_match = matches_event_pattern(test_event, test_pattern)
print(f"Pattern Match Result: {is_match}")
Step 4: Test Against AWS EventBridge API
Local validation is useful, but the definitive test is against the AWS EventBridge API. You can use the put_events API to send a test event and verify if it is delivered to a target (like an SQS queue or Lambda). However, a simpler debugging approach is to use the describe_rule and list_targets_by_rule to ensure the rule is active and has targets.
More importantly, you can use the put_events API to send your simulated event to EventBridge and check the Entries response. If the EventId is returned, the event was accepted. If you have a target configured, you can check the target’s logs.
import boto3
from botocore.exceptions import ClientError
def test_eventbridge_rule(event_payload: dict, rule_name: str):
"""
Sends a test event to EventBridge to verify rule matching.
Args:
event_payload: The event dictionary to send.
rule_name: The name of the EventBridge rule to test against.
"""
client = boto3.client('events', region_name='us-east-1')
# Convert the payload to a string for the API
event_string = json.dumps(event_payload)
try:
response = client.put_events(
Entries=[
{
'Source': event_payload['source'],
'DetailType': event_payload['detail-type'],
'Detail': event_string,
'EventBusName': 'default'
},
]
)
if response['FailedEntryCount'] > 0:
for error in response['Entries']:
print(f"Error sending event: {error.get('ErrorCode')} - {error.get('ErrorMessage')}")
else:
print(f"Event sent successfully. EventId: {response['Entries'][0]['EventId']}")
print("Check your EventBridge Rule targets (Lambda/SQS) to see if it fired.")
except ClientError as e:
print(f"AWS API Error: {e.response['Error']['Code']} - {e.response['Error']['Message']}")
Complete Working Example
Combine all the steps into a single script that fetches real data, constructs a payload, validates it locally, and optionally sends it to AWS.
import os
import json
import datetime
import uuid
import boto3
from purecloud_platform_client_v2 import PlatformClientBuilder, UserApi, QueueApi
from purecloud_platform_client_v2.rest import ApiException
from botocore.exceptions import ClientError
# --- Configuration ---
GENESYS_CLIENT_ID = os.getenv("GENESYS_CLIENT_ID")
GENESYS_CLIENT_SECRET = os.getenv("GENESYS_CLIENT_SECRET")
AWS_ACCOUNT_ID = os.getenv("AWS_ACCOUNT_ID", "123456789012")
AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
RULE_NAME = os.getenv("EVENTBRIDGE_RULE_NAME", "GenesysConversationRule")
# --- Helper Functions ---
def get_genesys_client():
builder = PlatformClientBuilder()
builder.set_client_id(GENESYS_CLIENT_ID)
builder.set_client_secret(GENESYS_CLIENT_SECRET)
builder.set_base_url("https://api.mypurecloud.com")
try:
client = builder.build()
client.login()
return client
except ApiException as e:
print(f"Authentication failed: {e.status} - {e.reason}")
raise SystemExit("Check your OAuth credentials.")
def fetch_sample_data(client):
user_api = client.UserApi()
queue_api = client.QueueApi()
try:
users = user_api.get_users(page_size=1)
queues = queue_api.get_queues(page_size=1)
if not users.entities or not queues.entities:
raise Exception("No users or queues found.")
return users.entities[0], queues.entities[0]
except ApiException as e:
print(f"Failed to fetch sample data: {e.status}")
raise
def construct_conversation_created_event(user, queue, aws_account_id):
conv_id = str(uuid.uuid4())
current_time = datetime.datetime.utcnow().isoformat() + "Z"
return {
"id": str(uuid.uuid4()),
"source": "genesys.cloud",
"account": aws_account_id,
"time": current_time,
"region": "us-east-1",
"detail-type": "conversation.created",
"resources": [
f"arn:aws:genesyscloud:{aws_account_id}:conversation:{conv_id}"
],
"detail": {
"id": conv_id,
"type": "voice",
"state": "initiated",
"direction": "inbound",
"priority": 0,
"origin": {
"type": "phone",
"phone": {
"phoneNumber": "+15551234567"
}
},
"wrapupRequired": False,
"routing": {
"skillRequirements": {},
"queue": {
"id": queue.id,
"name": queue.name
},
"skillGroup": None,
"media": "voice",
"skillAssignments": []
},
"participants": [
{
"id": str(uuid.uuid4()),
"name": user.name,
"type": "customer",
"address": {
"type": "phone",
"phone": {
"phoneNumber": "+15551234567"
}
},
"state": "connected",
"stateChangedTimestamp": current_time
}
],
"createdTimestamp": current_time,
"updatedTimestamp": current_time
}
}
def matches_event_pattern(event, pattern):
if not pattern or pattern == "*":
return True
if isinstance(pattern, list):
return event in pattern
if isinstance(pattern, dict):
if not isinstance(event, dict):
return False
for key, pattern_value in pattern.items():
if key not in event:
if pattern_value != "*":
return False
if not matches_event_pattern(event[key], pattern_value):
return False
return True
return event == pattern
def test_eventbridge_rule(event_payload):
client = boto3.client('events', region_name=AWS_REGION)
event_string = json.dumps(event_payload)
try:
response = client.put_events(
Entries=[
{
'Source': event_payload['source'],
'DetailType': event_payload['detail-type'],
'Detail': event_string,
'EventBusName': 'default'
},
]
)
if response['FailedEntryCount'] > 0:
for error in response['Entries']:
print(f"Error sending event: {error.get('ErrorCode')} - {error.get('ErrorMessage')}")
else:
print(f"Event sent successfully. EventId: {response['Entries'][0]['EventId']}")
except ClientError as e:
print(f"AWS API Error: {e.response['Error']['Code']} - {e.response['Error']['Message']}")
# --- Main Execution ---
if __name__ == "__main__":
# 1. Authenticate
print("Authenticating with Genesys Cloud...")
genesys_client = get_genesys_client()
# 2. Fetch Real Data
print("Fetching sample user and queue...")
sample_user, sample_queue = fetch_sample_data(genesys_client)
print(f"Using User: {sample_user.name} (ID: {sample_user.id})")
print(f"Using Queue: {sample_queue.name} (ID: {sample_queue.id})")
# 3. Construct Event
print("Constructing event payload...")
test_event = construct_conversation_created_event(sample_user, sample_queue, AWS_ACCOUNT_ID)
# 4. Define Pattern
print("Defining rule pattern...")
test_pattern = {
"source": ["genesys.cloud"],
"detail-type": ["conversation.created"],
"detail": {
"type": ["voice"],
"routing": {
"queue": {
"id": [sample_queue.id]
}
}
}
}
# 5. Local Validation
print("Validating pattern locally...")
is_match = matches_event_pattern(test_event, test_pattern)
print(f"Local Match Result: {is_match}")
if is_match:
# 6. Send to AWS
print("Sending event to AWS EventBridge...")
test_eventbridge_rule(test_event)
else:
print("Local validation failed. Do not send to AWS.")
print("Event Payload:")
print(json.dumps(test_event, indent=2))
print("Rule Pattern:")
print(json.dumps(test_pattern, indent=2))
Common Errors & Debugging
Error: Pattern Mismatch on detail.routing.queue.id
What causes it: The routing.queue object is null or missing in some conversation types (e.g., direct messages or unassigned chats). If your pattern requires queue.id to exist, events without a queue will not match.
How to fix it: Use a wildcard for the queue if you want to catch all conversations, or add a condition to check if routing.queue is not null.
# Corrected pattern for optional queue
test_pattern = {
"source": ["genesys.cloud"],
"detail-type": ["conversation.created"],
"detail": {
"type": ["voice"]
# Removed nested queue check
}
}
Error: 403 Forbidden on put_events
What causes it: Your AWS IAM role or user does not have the events:PutEvents permission.
How to fix it: Ensure your IAM policy includes:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"events:PutEvents"
],
"Resource": [
"arn:aws:events:us-east-1:123456789012:event-bus/default"
]
}
]
}
Error: Event Received but Rule Not Firing
What causes it: The rule exists and is enabled, but the target (Lambda/SQS) is failing or the rule pattern is too specific.
How to fix it:
- Check the CloudWatch Logs for the target Lambda function.
- Verify the
detail-typein your event matches the rule exactly. Genesys Cloud usesconversation.created,conversation.updated, andconversation.ended. Do not useConversation.Created(capitalized). - Check if the rule is enabled. A disabled rule will not fire.
# Check rule status
client = boto3.client('events', region_name=AWS_REGION)
response = client.describe_rule(Name=RULE_NAME)
print(f"Rule State: {response['State']}")