Implementing Dynamic Branching Logic with ASSIGN and IF Actions in CXone Studio
What You Will Build
- This tutorial builds a CXone Studio flow that dynamically routes inbound calls based on caller ID patterns and custom data attributes.
- It utilizes the CXone Studio Visual Flow API and the Studio Snippet language to demonstrate programmatic flow construction.
- The implementation covers Python for API interactions and CXone Studio Snippet syntax for in-flow logic.
Prerequisites
- OAuth Client Type: Internal Client or Client Credentials Grant.
- Required Scopes:
studio:flow:read,studio:flow:write,studio:flow:execute. - SDK Version: NICE CXone Python SDK
@nice-dcv/sdk(latest stable) or direct REST API usage. - Language/Runtime: Python 3.9+ with
requestsandjsonlibraries. - External Dependencies:
nice-dcv-sdk(optional, but recommended for type safety) or rawrequests. - CXone Environment: A valid CXone tenant with Studio access enabled.
Authentication Setup
CXone uses OAuth 2.0 for API authentication. For programmatic flow construction, the Client Credentials Grant is the standard approach. You must obtain an access token before issuing any Studio API requests.
The following Python snippet demonstrates how to acquire and cache a token. In production, implement a refresh mechanism or a token cache to avoid unnecessary network calls.
import requests
import json
from typing import Optional
class CXoneAuth:
def __init__(self, client_id: str, client_secret: str, region: str = "us"):
self.client_id = client_id
self.client_secret = client_secret
# Map region to OAuth endpoint
oauth_endpoints = {
"us": "https://api.mynicecx.com/oauth2/token",
"eu": "https://api.eu.mynicecx.com/oauth2/token",
"au": "https://api.ap.mynicecx.com/oauth2/token"
}
self.token_url = oauth_endpoints.get(region, oauth_endpoints["us"])
self.access_token: Optional[str] = None
self.headers: dict = {}
def get_access_token(self) -> str:
"""
Retrieves an OAuth2 access token using Client Credentials Grant.
Returns the token string.
"""
if self.access_token:
return self.access_token
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
try:
response = requests.post(
self.token_url,
data=payload,
headers={"Content-Type": "application/x-www-form-urlencoded"}
)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
self.headers = {
"Authorization": f"Bearer {self.access_token}",
"Content-Type": "application/json"
}
return self.access_token
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise Exception("Authentication failed: Invalid client ID or secret.")
elif response.status_code == 429:
raise Exception("Rate limit exceeded. Retry after delay.")
else:
raise Exception(f"Failed to get token: {e}")
except requests.exceptions.ConnectionError:
raise Exception("Network error connecting to CXone OAuth endpoint.")
# Usage
auth = CXoneAuth(client_id="your_client_id", client_secret="your_client_secret")
token = auth.get_access_token()
Implementation
Step 1: Constructing the ASSIGN Action
The ASSIGN action in CXone Studio is used to set variables, modify caller data, or prepare data for downstream actions. In the Studio JSON schema, this is represented as a node with type: "assign".
When building flows via API, you must define the assigns array. Each object in this array contains a variable name and a value. The value can be a literal string, a number, or a Snippet expression (enclosed in {{ }}).
The following code creates a minimal Studio flow definition containing a single ASSIGN action that sets a custom variable call_priority based on a static value.
import requests
def create_assign_node(variable_name: str, value: str) -> dict:
"""
Generates a JSON-serializable dictionary for a Studio ASSIGN node.
Args:
variable_name: The name of the CXone variable to set (e.g., 'call_priority').
value: The value to assign. Can be a literal or a Snippet expression.
Returns:
A dictionary representing the Studio node.
"""
assign_node = {
"type": "assign",
"name": f"Set {variable_name}",
"assigns": [
{
"variable": variable_name,
"value": value
}
]
}
return assign_node
# Example: Setting a literal value
literal_assign = create_assign_node("call_priority", "High")
# Example: Setting a value using Snippet syntax (Caller ID)
# Note: In Studio UI, this is {{ caller.id }}. In API JSON, it is often passed as a string literal that Studio evaluates.
snippet_assign = create_assign_node("caller_number", "{{ caller.id }}")
print(json.dumps(literal_assign, indent=2))
Expected Response Structure (Node Definition):
{
"type": "assign",
"name": "Set call_priority",
"assigns": [
{
"variable": "call_priority",
"value": "High"
}
]
}
Error Handling:
If the variable name contains invalid characters (e.g., spaces without proper quoting in Snippet context, or reserved keywords), the Studio API may return a 400 Bad Request when you attempt to save the flow. Always validate variable names against CXone naming conventions (alphanumeric and underscores only).
Step 2: Implementing the IF Action for Branching
The IF action evaluates a boolean condition. If true, the flow proceeds to the true port; otherwise, it proceeds to the false port. In the Studio JSON schema, an IF node must define conditions, truePort, and falsePort.
The condition is typically defined using Snippet syntax. For example, {{ caller.id == "5551234567" }}.
Crucially, when defining an IF node via API, you must ensure that the ports referenced in truePort and falsePort actually exist in the flow definition. You cannot reference a port on a node that has not been defined or connected.
def create_if_node(condition_snippet: str, true_port_name: str, false_port_name: str) -> dict:
"""
Generates a JSON-serializable dictionary for a Studio IF node.
Args:
condition_snippet: The Snippet expression to evaluate (e.g., '{{ caller.id == "5551234567" }}').
true_port_name: The name of the port to connect if the condition is true.
false_port_name: The name of the port to connect if the condition is false.
Returns:
A dictionary representing the Studio IF node.
"""
if_node = {
"type": "if",
"name": "Check Caller ID",
"conditions": [
{
"snippet": condition_snippet
}
],
"truePort": true_port_name,
"falsePort": false_port_name
}
return if_node
# Example: Check if caller ID matches a specific VIP number
vip_condition = "{{ caller.id == '+15551234567' }}"
vip_if_node = create_if_node(vip_condition, "vip_route", "standard_route")
print(json.dumps(vip_if_node, indent=2))
Expected Response Structure (Node Definition):
{
"type": "if",
"name": "Check Caller ID",
"conditions": [
{
"snippet": "{{ caller.id == '+15551234567' }}"
}
],
"truePort": "vip_route",
"falsePort": "standard_route"
}
Edge Cases:
- Null Values: If
caller.idis null (e.g., anonymous call), the Snippet evaluation may returnfalseor throw an error depending on strictness settings. Use{{ caller.id is not null and caller.id == '+15551234567' }}for robustness. - Data Types: Ensure the data types match. Comparing a string
"123"to a number123may yield unexpected results in Snippet engine. Use{{ caller.id == "+15551234567" }}(string comparison) rather than{{ caller.id == 15551234567 }}.
Step 3: Connecting Nodes and Publishing the Flow
Creating individual nodes is insufficient. You must assemble them into a valid flow structure with a start node, connections, and an end node. Then, you must POST this structure to the CXone Studio API.
The following code constructs a complete flow: Start → Assign → IF → End (True) / End (False).
def build_complete_flow(auth: CXoneAuth, flow_name: str, flow_id: Optional[str] = None) -> dict:
"""
Builds and publishes a complete CXone Studio flow.
Args:
auth: An authenticated CXoneAuth instance.
flow_name: The name of the flow.
flow_id: Optional ID for updating an existing flow. If None, creates a new flow.
Returns:
The API response containing the flow ID and status.
"""
# 1. Define Nodes
start_node = {
"type": "start",
"name": "Start",
"outPort": "assign_priority"
}
assign_node = create_assign_node("call_priority", "Standard")
if_node = create_if_node(
condition_snippet="{{ caller.id == '+15551234567' }}",
true_port_name="vip_end",
false_port_name="standard_end"
)
vip_end_node = {
"type": "end",
"name": "VIP Route End",
"id": "vip_end" # Explicit ID for port reference
}
standard_end_node = {
"type": "end",
"name": "Standard Route End",
"id": "standard_end"
}
# 2. Assemble Flow Structure
flow_definition = {
"name": flow_name,
"description": "Dynamic branching based on Caller ID",
"type": "studio",
"nodes": [
start_node,
assign_node,
if_node,
vip_end_node,
standard_end_node
],
"connections": [
{
"fromNode": "start",
"fromPort": "outPort",
"toNode": assign_node["name"],
"toPort": "in"
},
{
"fromNode": assign_node["name"],
"fromPort": "outPort",
"toNode": if_node["name"],
"toPort": "in"
},
{
"fromNode": if_node["name"],
"fromPort": "truePort",
"toNode": vip_end_node["id"],
"toPort": "in"
},
{
"fromNode": if_node["name"],
"fromPort": "falsePort",
"toNode": standard_end_node["id"],
"toPort": "in"
}
]
}
# 3. API Endpoint
base_url = "https://api.mynicecx.com/api/v2/studio/flows"
endpoint = f"{base_url}/{flow_id}" if flow_id else base_url
method = "PUT" if flow_id else "POST"
try:
response = requests.request(
method,
endpoint,
json=flow_definition,
headers=auth.headers
)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
print(f"API Error: {e}")
print(f"Response Body: {response.text}")
raise
Complete Working Example:
import requests
import json
from typing import Optional
class CXoneAuth:
def __init__(self, client_id: str, client_secret: str):
self.client_id = client_id
self.client_secret = client_secret
self.token_url = "https://api.mynicecx.com/oauth2/token"
self.access_token: Optional[str] = None
self.headers: dict = {}
def get_access_token(self) -> str:
if self.access_token:
return self.access_token
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
response = requests.post(
self.token_url,
data=payload,
headers={"Content-Type": "application/x-www-form-urlencoded"}
)
response.raise_for_status()
self.access_token = response.json()["access_token"]
self.headers = {
"Authorization": f"Bearer {self.access_token}",
"Content-Type": "application/json"
}
return self.access_token
def create_assign_node(variable_name: str, value: str) -> dict:
return {
"type": "assign",
"name": f"Set {variable_name}",
"assigns": [{"variable": variable_name, "value": value}]
}
def create_if_node(condition_snippet: str, true_port_name: str, false_port_name: str) -> dict:
return {
"type": "if",
"name": "Check Caller ID",
"conditions": [{"snippet": condition_snippet}],
"truePort": true_port_name,
"falsePort": false_port_name
}
def publish_branching_flow(client_id: str, client_secret: str, flow_name: str):
auth = CXoneAuth(client_id, client_secret)
auth.get_access_token()
start_node = {"type": "start", "name": "Start", "outPort": "assign_priority"}
assign_node = create_assign_node("call_priority", "Standard")
if_node = create_if_node(
condition_snippet="{{ caller.id == '+15551234567' }}",
true_port_name="vip_end",
false_port_name="standard_end"
)
vip_end_node = {"type": "end", "name": "VIP Route End", "id": "vip_end"}
standard_end_node = {"type": "end", "name": "Standard Route End", "id": "standard_end"}
flow_definition = {
"name": flow_name,
"description": "Dynamic branching based on Caller ID",
"type": "studio",
"nodes": [start_node, assign_node, if_node, vip_end_node, standard_end_node],
"connections": [
{"fromNode": "start", "fromPort": "outPort", "toNode": assign_node["name"], "toPort": "in"},
{"fromNode": assign_node["name"], "fromPort": "outPort", "toNode": if_node["name"], "toPort": "in"},
{"fromNode": if_node["name"], "fromPort": "truePort", "toNode": vip_end_node["id"], "toPort": "in"},
{"fromNode": if_node["name"], "fromPort": "falsePort", "toNode": standard_end_node["id"], "toPort": "in"}
]
}
endpoint = "https://api.mynicecx.com/api/v2/studio/flows"
response = requests.post(endpoint, json=flow_definition, headers=auth.headers)
if response.status_code == 200 or response.status_code == 201:
print(f"Flow published successfully. ID: {response.json().get('id')}")
else:
print(f"Failed to publish flow. Status: {response.status_code}")
print(response.text)
if __name__ == "__main__":
# Replace with your actual credentials
CLIENT_ID = "your_client_id"
CLIENT_SECRET = "your_client_secret"
publish_branching_flow(CLIENT_ID, CLIENT_SECRET, "Caller ID Branching Flow")
Common Errors & Debugging
Error: 400 Bad Request - Invalid Node Configuration
- Cause: The
IFnode references atruePortorfalsePortthat does not match any nodeidornamein thenodesarray. Alternatively, theconnectionsarray has a mismatchedfromPortortoPort. - Fix: Verify that every port referenced in
connectionsexists in thenodesarray. Ensure thatIFnode ports (truePort,falsePort) point to theidof the target node. - Code Fix: Check the
idfield in your target nodes. If you omitid, CXone generates one, but for API construction, explicitidfields are safer for reference.
Error: 422 Unprocessable Entity - Snippet Syntax Error
- Cause: The Snippet expression in the
IFcondition is malformed. For example, missing quotes around string literals or incorrect variable names. - Fix: Validate the Snippet syntax. Ensure strings are quoted:
{{ caller.id == "123" }}. Ensure variables exist:{{ custom.my_var == "value" }}. - Debugging: Use the CXone Studio UI “Validate Flow” button if you can import the JSON, or check the API response body for specific Snippet compilation errors.
Error: 403 Forbidden - Insufficient Permissions
- Cause: The OAuth client lacks the
studio:flow:writescope. - Fix: Update your OAuth client configuration in the CXone Admin Portal to include the required Studio scopes.