Writing a CXone Studio Script That Performs a Database Lookup via DBConnector
What You Will Build
- A CXone Studio workflow script that queries an external database using the DBConnector action and routes execution based on the returned record set.
- The CXone Studio API (
/api/v2/scripts) and Pythonhttpxfor programmatic script deployment and validation. - Python 3.9+ with
httpxandpydanticfor type-safe payload construction and HTTP client management.
Prerequisites
- CXone OAuth2 client credentials with
studio:scripts:writeandstudio:scripts:readscopes. - CXone API v2 (current stable release).
- Python 3.9+,
httpx>=0.24.0,pydantic>=2.0.0. - A preconfigured DBConnector instance in CXone Admin (requires a valid
connectionId). - Network access to
https://{{domain}}.api.cxone.com.
Authentication Setup
CXone uses standard OAuth2 client credentials flow for server-to-server API access. The token endpoint is /api/v2/oauth/token. You must cache the access token and handle expiration. The following implementation includes token caching, expiration tracking, and automatic retry logic for HTTP 429 rate-limit responses.
import time
import httpx
from typing import Optional
CXONE_BASE_URL = "https://myorg.api.cxone.com"
OAUTH_TOKEN_URL = f"{CXONE_BASE_URL}/api/v2/oauth/token"
class CXoneAuthClient:
def __init__(self, client_id: str, client_secret: str, scopes: list[str]):
self.client_id = client_id
self.client_secret = client_secret
self.scopes = scopes
self._token: Optional[str] = None
self._expires_at: float = 0.0
self.http = httpx.Client(base_url=CXONE_BASE_URL, timeout=30.0)
def _get_token(self) -> str:
if self._token and time.time() < self._expires_at:
return self._token
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": " ".join(self.scopes)
}
response = self.http.post(OAUTH_TOKEN_URL, data=payload)
response.raise_for_status()
data = response.json()
self._token = data["access_token"]
self._expires_at = time.time() + data["expires_in"] - 30 # 30s buffer
return self._token
def get_auth_headers(self) -> dict:
return {
"Authorization": f"Bearer {self._get_token()}",
"Content-Type": "application/json",
"Accept": "application/json"
}
def request_with_retry(self, method: str, path: str, **kwargs) -> httpx.Response:
max_retries = 3
for attempt in range(max_retries):
response = self.http.request(method, path, headers=self.get_auth_headers(), **kwargs)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
print(f"Rate limited. Retrying in {retry_after}s (attempt {attempt + 1}/{max_retries})")
time.sleep(retry_after)
continue
return response
raise httpx.HTTPStatusError("Max retries exceeded for 429", request=response.request, response=response)
The request_with_retry method handles 429 responses by reading the Retry-After header or falling back to exponential backoff. This prevents cascading rate-limit failures during bulk script deployments.
Implementation
Step 1: Define the Studio Script JSON Structure
CXone Studio scripts are authored as JSON documents containing blocks, variables, and transitions. The DBConnector action requires a connectionId, a SQL query string, parameter bindings, and output variable mapping. The following payload defines a script that accepts a customer identifier, queries an external database, and branches based on the result.
{
"name": "CustomerDBLookup",
"description": "Performs a database lookup via DBConnector and routes based on result count",
"version": "1.0.0",
"entryBlock": "Start",
"variables": [
{"name": "input.customerId", "type": "String", "value": ""},
{"name": "db.result", "type": "Object", "value": null},
{"name": "db.recordCount", "type": "Number", "value": 0}
],
"blocks": [
{
"name": "Start",
"type": "Start",
"properties": {},
"transitions": {"next": "DBLookup"}
},
{
"name": "DBLookup",
"type": "DBConnector",
"properties": {
"connectionId": "conn_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"query": "SELECT customer_id, first_name, loyalty_tier FROM customers WHERE customer_id = ?",
"parameters": ["{{input.customerId}}"],
"outputVariable": "db.result",
"timeout": 5000
},
"transitions": {
"success": "CheckRecordCount",
"error": "HandleDBError"
}
},
{
"name": "CheckRecordCount",
"type": "Condition",
"properties": {
"conditions": [
{"left": "{{db.result.length}}", "operator": "GREATER_THAN", "right": "0"}
]
},
"transitions": {
"true": "ProcessCustomer",
"false": "NoRecordFound"
}
},
{
"name": "ProcessCustomer",
"type": "SetVariable",
"properties": {
"variableName": "db.recordCount",
"value": "{{db.result.length}}"
},
"transitions": {"next": "EndSuccess"}
},
{
"name": "NoRecordFound",
"type": "SetVariable",
"properties": {
"variableName": "db.recordCount",
"value": "0"
},
"transitions": {"next": "EndSuccess"}
},
{
"name": "HandleDBError",
"type": "SetVariable",
"properties": {
"variableName": "error.message",
"value": "{{error.description}}"
},
"transitions": {"next": "EndError"}
},
{
"name": "EndSuccess",
"type": "End",
"properties": {"result": "SUCCESS"},
"transitions": {}
},
{
"name": "EndError",
"type": "End",
"properties": {"result": "ERROR"},
"transitions": {}
}
]
}
The parameters array uses CXone expression syntax ({{input.customerId}}) to bind runtime values to the SQL query. The outputVariable captures the result set as a JSON array. The timeout property prevents blocking the Studio runtime during slow database queries.
Step 2: Configure DBConnector Parameters and Transitions
The DBConnector action validates the SQL syntax against the registered database schema during deployment. You must ensure the connectionId matches a DBConnector instance configured in CXone Admin. Parameter binding uses zero-indexed placeholders (?) in the query string. Each placeholder maps to a corresponding index in the parameters array.
When the DBConnector executes, CXone evaluates the transitions object. The success transition triggers when the query returns without SQL errors, regardless of row count. The error transition triggers on connection failures, timeout violations, or syntax errors. Always define both transitions to prevent orphaned execution paths.
Step 3: Deploy the Script via API with Validation
The CXone Studio API accepts script definitions via POST /api/v2/scripts. The endpoint performs structural validation before persistence. You must include the Content-Type: application/json header and pass the script payload in the request body. The following Python code demonstrates the deployment call with comprehensive error handling.
import httpx
from typing import Any, Dict
def deploy_studio_script(auth_client: CXoneAuthClient, script_payload: Dict[str, Any]) -> Dict[str, Any]:
path = "/api/v2/scripts"
request_headers = {
"Authorization": f"Bearer {auth_client._get_token()}",
"Content-Type": "application/json",
"Accept": "application/json"
}
response = auth_client.request_with_retry("POST", path, json=script_payload, headers=request_headers)
if response.status_code == 201:
return response.json()
elif response.status_code == 400:
error_body = response.json()
raise ValueError(f"Validation failed: {error_body.get('description', 'Unknown validation error')}")
elif response.status_code == 409:
raise ConflictError(f"Script name already exists: {script_payload['name']}")
else:
response.raise_for_status()
class ConflictError(Exception):
pass
HTTP Request Cycle Example
POST /api/v2/scripts HTTP/1.1
Host: myorg.api.cxone.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
Accept: application/json
{
"name": "CustomerDBLookup",
"description": "Performs a database lookup via DBConnector and routes based on result count",
"version": "1.0.0",
"entryBlock": "Start",
"variables": [...],
"blocks": [...]
}
HTTP Response Cycle Example (Success)
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v2/scripts/script_x9y8z7w6-v5u4-3210-abcd-ef1234567890
Date: Mon, 15 Oct 2024 14:32:00 GMT
{
"id": "script_x9y8z7w6-v5u4-3210-abcd-ef1234567890",
"name": "CustomerDBLookup",
"version": "1.0.0",
"status": "VALID",
"createdTimestamp": 1729000320000,
"modifiedTimestamp": 1729000320000
}
The API returns 201 Created with a Location header pointing to the resource URI. The status field indicates VALID when the JSON structure passes schema validation. If the DBConnector connectionId references a disabled connection, the API returns 400 with a detailed validation error.
Step 4: List Scripts with Pagination
The CXone Studio API supports cursor-based pagination for collection endpoints. You must handle the nextPageToken to retrieve all scripts in large environments. The following implementation demonstrates pagination with automatic continuation.
def list_all_scripts(auth_client: CXoneAuthClient, page_size: int = 50) -> list[Dict[str, Any]]:
all_scripts = []
next_token: Optional[str] = None
while True:
params = {"pageSize": page_size}
if next_token:
params["nextPageToken"] = next_token
response = auth_client.request_with_retry("GET", "/api/v2/scripts", params=params)
response.raise_for_status()
data = response.json()
all_scripts.extend(data.get("entities", []))
next_token = data.get("nextPageToken")
if not next_token:
break
return all_scripts
The nextPageToken field is omitted from the response when the final page is reached. This pattern prevents infinite loops and ensures complete dataset retrieval without manual offset calculation.
Complete Working Example
The following module combines authentication, script definition, deployment, and listing into a single executable workflow. Replace the credential placeholders before execution.
import time
import httpx
from typing import Optional, Any, Dict
CXONE_BASE_URL = "https://myorg.api.cxone.com"
OAUTH_TOKEN_URL = f"{CXONE_BASE_URL}/api/v2/oauth/token"
class CXoneAuthClient:
def __init__(self, client_id: str, client_secret: str, scopes: list[str]):
self.client_id = client_id
self.client_secret = client_secret
self.scopes = scopes
self._token: Optional[str] = None
self._expires_at: float = 0.0
self.http = httpx.Client(base_url=CXONE_BASE_URL, timeout=30.0)
def _get_token(self) -> str:
if self._token and time.time() < self._expires_at:
return self._token
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": " ".join(self.scopes)
}
response = self.http.post(OAUTH_TOKEN_URL, data=payload)
response.raise_for_status()
data = response.json()
self._token = data["access_token"]
self._expires_at = time.time() + data["expires_in"] - 30
return self._token
def request_with_retry(self, method: str, path: str, **kwargs) -> httpx.Response:
max_retries = 3
for attempt in range(max_retries):
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {self._get_token()}"
response = self.http.request(method, path, headers=headers, **kwargs)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
time.sleep(retry_after)
continue
return response
raise httpx.HTTPStatusError("Max retries exceeded", request=response.request, response=response)
def deploy_studio_script(auth_client: CXoneAuthClient, script_payload: Dict[str, Any]) -> Dict[str, Any]:
path = "/api/v2/scripts"
response = auth_client.request_with_retry("POST", path, json=script_payload, headers={"Accept": "application/json"})
if response.status_code == 201:
return response.json()
elif response.status_code == 400:
raise ValueError(f"Validation failed: {response.json().get('description')}")
else:
response.raise_for_status()
def main():
auth = CXoneAuthClient(
client_id="your_client_id",
client_secret="your_client_secret",
scopes=["studio:scripts:write", "studio:scripts:read"]
)
script_definition = {
"name": "CustomerDBLookup",
"description": "Performs a database lookup via DBConnector and routes based on result count",
"version": "1.0.0",
"entryBlock": "Start",
"variables": [
{"name": "input.customerId", "type": "String", "value": ""},
{"name": "db.result", "type": "Object", "value": None},
{"name": "db.recordCount", "type": "Number", "value": 0}
],
"blocks": [
{"name": "Start", "type": "Start", "properties": {}, "transitions": {"next": "DBLookup"}},
{
"name": "DBLookup",
"type": "DBConnector",
"properties": {
"connectionId": "conn_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"query": "SELECT customer_id, first_name, loyalty_tier FROM customers WHERE customer_id = ?",
"parameters": ["{{input.customerId}}"],
"outputVariable": "db.result",
"timeout": 5000
},
"transitions": {"success": "CheckRecordCount", "error": "HandleDBError"}
},
{
"name": "CheckRecordCount",
"type": "Condition",
"properties": {"conditions": [{"left": "{{db.result.length}}", "operator": "GREATER_THAN", "right": "0"}]},
"transitions": {"true": "ProcessCustomer", "false": "NoRecordFound"}
},
{"name": "ProcessCustomer", "type": "SetVariable", "properties": {"variableName": "db.recordCount", "value": "{{db.result.length}}"}, "transitions": {"next": "EndSuccess"}},
{"name": "NoRecordFound", "type": "SetVariable", "properties": {"variableName": "db.recordCount", "value": "0"}, "transitions": {"next": "EndSuccess"}},
{"name": "HandleDBError", "type": "SetVariable", "properties": {"variableName": "error.message", "value": "{{error.description}}"}, "transitions": {"next": "EndError"}},
{"name": "EndSuccess", "type": "End", "properties": {"result": "SUCCESS"}, "transitions": {}},
{"name": "EndError", "type": "End", "properties": {"result": "ERROR"}, "transitions": {}}
]
}
try:
result = deploy_studio_script(auth, script_definition)
print(f"Script deployed successfully: {result['id']}")
except Exception as e:
print(f"Deployment failed: {e}")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: HTTP 400 Bad Request
- Cause: The script JSON violates the CXone Studio schema. Common triggers include missing
transitions, invalidconnectionId, or mismatched parameter counts in the DBConnectorqueryandparametersarrays. - Fix: Validate the JSON structure against the Studio schema. Ensure every block defines all required transitions. Verify the DBConnector connection exists and is enabled in Admin.
- Code showing the fix:
if response.status_code == 400:
errors = response.json().get("validationErrors", [])
for err in errors:
print(f"Block {err.get('blockName')}: {err.get('message')}")
Error: HTTP 401 Unauthorized / 403 Forbidden
- Cause: The OAuth token expired, or the client credentials lack
studio:scripts:writescope. - Fix: Regenerate the token using the
_get_tokenmethod. Verify the OAuth client configuration in CXone Admin includes the required scopes. - Code showing the fix:
# Token refresh is automatic in _get_token()
# Verify scopes during initialization
auth = CXoneAuthClient(client_id, client_secret, scopes=["studio:scripts:write"])
Error: HTTP 429 Too Many Requests
- Cause: Exceeded CXone API rate limits. Studio script deployments count against the organization-wide API quota.
- Fix: Implement exponential backoff. The
request_with_retrymethod handles this automatically by readingRetry-Afterand sleeping before the next attempt. - Code showing the fix:
# Already implemented in CXoneAuthClient.request_with_retry
# Returns after successful retry or raises HTTPStatusError on max retries
Error: DBConnector Runtime Timeout
- Cause: The database query exceeds the
timeoutproperty value (default 5000ms). Complex joins or unindexed columns cause slow execution. - Fix: Increase the
timeoutproperty to 10000ms or optimize the SQL query. Add database indexes on filtered columns. - Code showing the fix:
"properties": {
"connectionId": "conn_xxx",
"query": "SELECT ...",
"parameters": ["{{input.customerId}}"],
"outputVariable": "db.result",
"timeout": 10000
}