Building a Custom Data Action to Fetch External REST Data in Genesys Cloud Architect
What You Will Build
- You will build a custom Data Action that executes a synchronous HTTP GET request to an external REST API during a flow execution.
- You will use the Genesys Cloud Python SDK (
genesyscloud) to define the Data Action schema, validate inputs, and map the JSON response payload to specific Architect variables. - The implementation will use Python 3.9+ and the
httpxlibrary for robust, asynchronous-compatible HTTP handling within the synchronous Data Action context.
Prerequisites
- Genesys Cloud Account: You need an account with access to Architect and the ability to manage Integrations.
- OAuth Client: A Confidential Client (Client ID and Client Secret) registered in the Genesys Cloud Admin Portal with the scope
dataaction:readanddataaction:write. - Python Environment: Python 3.9 or higher installed locally.
- External API Endpoint: A publicly accessible REST endpoint that returns JSON. For this tutorial, we will use a mock user profile API.
- Dependencies: Install the Genesys Cloud Python SDK and HTTP client.
pip install genesyscloud httpx
Authentication Setup
Genesys Cloud Data Actions run in a secure, isolated environment. You do not need to manually handle OAuth token refresh logic inside the Data Action code itself, as the SDK handles the initial authentication and the platform manages the execution context. However, you must authenticate your development environment to push the Data Action to the platform.
First, configure your environment variables for the SDK.
export GENESYS_CLOUD_REGION="mypurecloud.com" # or "us-gov-purecloud.com", etc.
export GENESYS_CLOUD_CLIENT_ID="your-client-id"
export GENESYS_CLOUD_CLIENT_SECRET="your-client-secret"
Initialize the SDK client in your script. This client is used to register the Data Action, not to make the external API call.
import os
from genesyscloud import PureCloudPlatformClientV2
def get_platform_client():
"""
Initializes and returns the Genesys Cloud Platform Client.
"""
client = PureCloudPlatformClientV2.create(
client_id=os.environ.get("GENESYS_CLOUD_CLIENT_ID"),
client_secret=os.environ.get("GENESYS_CLOUD_CLIENT_SECRET"),
region=os.environ.get("GENESYS_CLOUD_REGION", "mypurecloud.com")
)
return client
Implementation
Step 1: Define the Data Action Schema
Before writing the execution logic, you must define the contract for the Data Action. This includes the inputs (variables passed from Architect) and outputs (variables returned to Architect).
The schema is defined using a dictionary that conforms to the Genesys Cloud Data Action specification. We will create an action named FetchUserProfile.
import json
import httpx
import logging
# Configure logging for debugging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def get_data_action_schema():
"""
Returns the JSON schema for the Data Action.
"""
schema = {
"name": "FetchUserProfile",
"description": "Fetches user profile data from an external REST API.",
"inputs": [
{
"name": "userId",
"type": "string",
"description": "The ID of the user to fetch.",
"required": True
},
{
"name": "apiEndpoint",
"type": "string",
"description": "The base URL of the external API.",
"required": True,
"defaultValue": "https://jsonplaceholder.typicode.com/users"
}
],
"outputs": [
{
"name": "fullName",
"type": "string",
"description": "The full name of the user."
},
{
"name": "email",
"type": "string",
"description": "The email address of the user."
},
{
"name": "companyName",
"type": "string",
"description": "The name of the user's company."
},
{
"name": "statusCode",
"type": "number",
"description": "The HTTP status code returned by the external API."
},
{
"name": "errorMessage",
"type": "string",
"description": "Error message if the request failed."
}
],
"execution": {
"type": "python",
"version": "3.9"
}
}
return schema
Key Design Decisions:
- Input Validation: We mark
userIdandapiEndpointas required. Architect will enforce this before the Data Action executes. - Output Mapping: We define specific fields (
fullName,email) rather than returning the raw JSON. This forces the Data Action to parse the response, reducing complexity in the Architect flow. - Error Handling Output: We include
statusCodeanderrorMessageas outputs. This allows the Architect flow to branch based on success or failure without stopping the entire flow.
Step 2: Implement the Execution Logic
The execute function is the entry point for the Data Action. It receives the input variables as a dictionary and must return a dictionary of output variables.
Critical Constraint: Genesys Cloud Data Actions have a strict timeout (typically 10-30 seconds). You must handle network latency and retries efficiently.
def execute(inputs: dict) -> dict:
"""
Main execution function for the Data Action.
Args:
inputs (dict): Dictionary containing input variables from Architect.
Returns:
dict: Dictionary containing output variables mapped to Architect variables.
"""
# Initialize outputs with defaults
outputs = {
"fullName": "",
"email": "",
"companyName": "",
"statusCode": 0,
"errorMessage": ""
}
# Extract inputs
user_id = inputs.get("userId")
api_endpoint = inputs.get("apiEndpoint")
# Validate inputs
if not user_id:
outputs["errorMessage"] = "userId is required."
return outputs
if not api_endpoint:
outputs["errorMessage"] = "apiEndpoint is required."
return outputs
# Construct the URL
# Ensure the endpoint does not already end with / to avoid double slashes
base_url = api_endpoint.rstrip('/')
url = f"{base_url}/{user_id}"
logger.info(f"Fetching user data from {url}")
try:
# Use httpx for synchronous request.
# Note: In Genesys Data Actions, use synchronous calls.
# Do not use async/await as the runtime is synchronous.
with httpx.Client(timeout=10.0) as client:
response = client.get(url)
outputs["statusCode"] = response.status_code
# Check for HTTP errors
if response.status_code != 200:
try:
error_body = response.json()
outputs["errorMessage"] = str(error_body)
except Exception:
outputs["errorMessage"] = response.text
logger.warning(f"HTTP Error {response.status_code}: {outputs['errorMessage']}")
return outputs
# Parse JSON response
try:
data = response.json()
# Map the JSON response to the defined outputs
outputs["fullName"] = data.get("name", "")
outputs["email"] = data.get("email", "")
# Nested object access for company name
company = data.get("company", {})
if isinstance(company, dict):
outputs["companyName"] = company.get("name", "")
else:
outputs["companyName"] = str(company)
logger.info(f"Successfully mapped data for user {user_id}")
except json.JSONDecodeError:
outputs["errorMessage"] = "Invalid JSON response from external API."
logger.error(outputs["errorMessage"])
except httpx.TimeoutException:
outputs["statusCode"] = 408
outputs["errorMessage"] = "Request to external API timed out."
logger.error(outputs["errorMessage"])
except httpx.RequestError as e:
outputs["statusCode"] = 500
outputs["errorMessage"] = f"Network error: {str(e)}"
logger.error(outputs["errorMessage"])
except Exception as e:
outputs["statusCode"] = 500
outputs["errorMessage"] = f"Unexpected error: {str(e)}"
logger.error(outputs["errorMessage"])
return outputs
Why httpx and not requests?
While requests is popular, httpx provides a consistent API for both synchronous and asynchronous code. In the context of Genesys Cloud Data Actions, we use the synchronous client. httpx also offers better default handling for SSL and redirects, which reduces potential connection errors in enterprise networks.
Mapping Strategy:
Notice how we use .get() with default values. If the external API returns a malformed JSON object missing the name field, the Data Action will not crash. It will return an empty string for fullName. This is crucial for flow stability. If the Data Action throws an unhandled exception, the entire Architect flow fails. Returning a structured error in the outputs allows the flow to continue and handle the error gracefully.
Step 3: Register the Data Action with Genesys Cloud
To make the Data Action available in Architect, you must push it to the Genesys Cloud platform. This involves creating a DataAction object and posting it to the API.
from genesyscloud.data_action import DataActionApi
from genesyscloud.models import DataAction
def register_data_action(platform_client):
"""
Registers the Data Action with Genesys Cloud.
"""
api = DataActionApi(platform_client)
# Get the schema
schema = get_data_action_schema()
# Create the DataAction object
# The 'code' field contains the Python code as a string
code_string = """
import json
import httpx
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def execute(inputs: dict) -> dict:
outputs = {
"fullName": "",
"email": "",
"companyName": "",
"statusCode": 0,
"errorMessage": ""
}
user_id = inputs.get("userId")
api_endpoint = inputs.get("apiEndpoint")
if not user_id:
outputs["errorMessage"] = "userId is required."
return outputs
if not api_endpoint:
outputs["errorMessage"] = "apiEndpoint is required."
return outputs
base_url = api_endpoint.rstrip('/')
url = f"{base_url}/{user_id}"
try:
with httpx.Client(timeout=10.0) as client:
response = client.get(url)
outputs["statusCode"] = response.status_code
if response.status_code != 200:
try:
error_body = response.json()
outputs["errorMessage"] = str(error_body)
except Exception:
outputs["errorMessage"] = response.text
return outputs
try:
data = response.json()
outputs["fullName"] = data.get("name", "")
outputs["email"] = data.get("email", "")
company = data.get("company", {})
if isinstance(company, dict):
outputs["companyName"] = company.get("name", "")
else:
outputs["companyName"] = str(company)
except json.JSONDecodeError:
outputs["errorMessage"] = "Invalid JSON response from external API."
except httpx.TimeoutException:
outputs["statusCode"] = 408
outputs["errorMessage"] = "Request to external API timed out."
except httpx.RequestError as e:
outputs["statusCode"] = 500
outputs["errorMessage"] = f"Network error: {str(e)}"
except Exception as e:
outputs["statusCode"] = 500
outputs["errorMessage"] = f"Unexpected error: {str(e)}"
return outputs
"""
data_action = DataAction(
name=schema["name"],
description=schema["description"],
inputs=schema["inputs"],
outputs=schema["outputs"],
execution={
"type": schema["execution"]["type"],
"version": schema["execution"]["version"],
"code": code_string
}
)
try:
# Create or update the Data Action
# If it already exists, you may need to fetch it first and update it
response = api.post_api_platform_data_action(body=data_action)
logger.info(f"Data Action '{schema['name']}' registered successfully. ID: {response.id}")
return response
except Exception as e:
logger.error(f"Failed to register Data Action: {e}")
raise e
if __name__ == "__main__":
client = get_platform_client()
register_data_action(client)
Complete Working Example
Below is the complete, consolidated script. Save this as register_fetch_user_action.py.
import os
import json
import httpx
import logging
from genesyscloud import PureCloudPlatformClientV2
from genesyscloud.data_action import DataActionApi
from genesyscloud.models import DataAction
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def get_platform_client():
"""
Initializes and returns the Genesys Cloud Platform Client.
"""
client_id = os.environ.get("GENESYS_CLOUD_CLIENT_ID")
client_secret = os.environ.get("GENESYS_CLOUD_CLIENT_SECRET")
region = os.environ.get("GENESYS_CLOUD_REGION", "mypurecloud.com")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLOUD_CLIENT_ID and GENESYS_CLOUD_CLIENT_SECRET must be set.")
client = PureCloudPlatformClientV2.create(
client_id=client_id,
client_secret=client_secret,
region=region
)
return client
def get_data_action_code():
"""
Returns the Python code string for the Data Action.
"""
return """
import json
import httpx
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def execute(inputs: dict) -> dict:
outputs = {
"fullName": "",
"email": "",
"companyName": "",
"statusCode": 0,
"errorMessage": ""
}
user_id = inputs.get("userId")
api_endpoint = inputs.get("apiEndpoint")
if not user_id:
outputs["errorMessage"] = "userId is required."
return outputs
if not api_endpoint:
outputs["errorMessage"] = "apiEndpoint is required."
return outputs
base_url = api_endpoint.rstrip('/')
url = f"{base_url}/{user_id}"
try:
with httpx.Client(timeout=10.0) as client:
response = client.get(url)
outputs["statusCode"] = response.status_code
if response.status_code != 200:
try:
error_body = response.json()
outputs["errorMessage"] = str(error_body)
except Exception:
outputs["errorMessage"] = response.text
return outputs
try:
data = response.json()
outputs["fullName"] = data.get("name", "")
outputs["email"] = data.get("email", "")
company = data.get("company", {})
if isinstance(company, dict):
outputs["companyName"] = company.get("name", "")
else:
outputs["companyName"] = str(company)
except json.JSONDecodeError:
outputs["errorMessage"] = "Invalid JSON response from external API."
except httpx.TimeoutException:
outputs["statusCode"] = 408
outputs["errorMessage"] = "Request to external API timed out."
except httpx.RequestError as e:
outputs["statusCode"] = 500
outputs["errorMessage"] = f"Network error: {str(e)}"
except Exception as e:
outputs["statusCode"] = 500
outputs["errorMessage"] = f"Unexpected error: {str(e)}"
return outputs
"""
def get_data_action_schema():
"""
Returns the JSON schema for the Data Action.
"""
return {
"name": "FetchUserProfile",
"description": "Fetches user profile data from an external REST API.",
"inputs": [
{
"name": "userId",
"type": "string",
"description": "The ID of the user to fetch.",
"required": True
},
{
"name": "apiEndpoint",
"type": "string",
"description": "The base URL of the external API.",
"required": True,
"defaultValue": "https://jsonplaceholder.typicode.com/users"
}
],
"outputs": [
{
"name": "fullName",
"type": "string",
"description": "The full name of the user."
},
{
"name": "email",
"type": "string",
"description": "The email address of the user."
},
{
"name": "companyName",
"type": "string",
"description": "The name of the user's company."
},
{
"name": "statusCode",
"type": "number",
"description": "The HTTP status code returned by the external API."
},
{
"name": "errorMessage",
"type": "string",
"description": "Error message if the request failed."
}
],
"execution": {
"type": "python",
"version": "3.9"
}
}
def register_data_action(platform_client):
"""
Registers the Data Action with Genesys Cloud.
"""
api = DataActionApi(platform_client)
schema = get_data_action_schema()
data_action = DataAction(
name=schema["name"],
description=schema["description"],
inputs=schema["inputs"],
outputs=schema["outputs"],
execution={
"type": schema["execution"]["type"],
"version": schema["execution"]["version"],
"code": get_data_action_code()
}
)
try:
response = api.post_api_platform_data_action(body=data_action)
logger.info(f"Data Action '{schema['name']}' registered successfully. ID: {response.id}")
return response
except Exception as e:
logger.error(f"Failed to register Data Action: {e}")
raise e
if __name__ == "__main__":
try:
client = get_platform_client()
register_data_action(client)
except Exception as e:
logger.error(f"Execution failed: {e}")
Common Errors & Debugging
Error: 403 Forbidden on Data Action Registration
- Cause: The OAuth client does not have the
dataaction:writescope. - Fix: Go to the Genesys Cloud Admin Portal > Integrations > OAuth > Edit your client. Add
dataaction:writeto the scopes and regenerate the secret if necessary.
Error: 500 Internal Server Error in Data Action Execution
- Cause: The Python code in the Data Action threw an unhandled exception.
- Fix: Check the Genesys Cloud Admin Portal > Architect > Data Actions > Select your action > Logs. The logs will show the exact Python traceback. Ensure your
try/exceptblocks cover all potential failure points, including JSON parsing errors.
Error: Data Action Timeout
- Cause: The external API took longer than 10-30 seconds to respond.
- Fix: Reduce the
httpx.Client(timeout=...)value to fail fast. If the external API is inherently slow, consider using a webhook-based approach instead of a synchronous Data Action.
Error: Variables Not Mapping in Architect
- Cause: The output keys in the Python
executefunction do not exactly match thenamefields in theoutputsschema. - Fix: Ensure the dictionary keys returned by
execute(e.g.,fullName) match thenamefield in the schema definition (e.g.,"name": "fullName"). Case sensitivity matters.