Implementing Deterministic Branching Logic in NICE CXone Studio Using ASSIGN and IF Actions
What You Will Build
- A Studio flow that captures user input, assigns it to a variable, and routes the interaction based on conditional logic using
ASSIGNandIFactions. - This tutorial uses the NICE CXone Studio API and the Studio Visual Editor concepts to demonstrate programmatic flow construction.
- The primary language covered is JavaScript/TypeScript for API interactions, with specific focus on the Studio Action configuration JSON payloads.
Prerequisites
- OAuth Client: A CXone API client with
studio:flows:readandstudio:flows:writescopes. - SDK Version: NICE CXone Studio SDK (Node.js or Python) or direct REST API calls.
- Language/Runtime: Node.js 18+ or Python 3.9+.
- External Dependencies:
- Node.js:
@nice-dx/cxone-studio-sdk - Python:
cxone-studio(if using the Python wrapper) orrequestsfor direct REST.
- Node.js:
Authentication Setup
NICE CXone uses OAuth 2.0 for API authentication. You must obtain an access token before making any Studio API calls. The token must include the studio:flows:write scope to modify flows.
import axios from 'axios';
const CXONE_DOMAIN = 'your-domain.nice-incontact.com';
const CLIENT_ID = process.env.CXONE_CLIENT_ID;
const CLIENT_SECRET = process.env.CXONE_CLIENT_SECRET;
async function getStudioToken() {
const url = `https://${CXONE_DOMAIN}/api/v2/oauth/token`;
const params = new URLSearchParams({
grant_type: 'client_credentials',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
scope: 'studio:flows:write studio:flows:read'
});
try {
const response = await axios.post(url, params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
return response.data.access_token;
} catch (error) {
console.error('Failed to acquire OAuth token:', error.response?.data || error.message);
throw error;
}
}
Store this token securely. It expires after 3600 seconds (1 hour). In production, implement a refresh mechanism or cache the token until expiration.
Implementation
Step 1: Define the Flow Structure and Variables
In CXone Studio, logic relies heavily on variables. Before branching, you must assign values to variables. The ASSIGN action creates or updates these variables. The IF action evaluates conditions against them.
First, we need a base flow. For this tutorial, we assume a new flow is being created or an existing one is being updated. We will focus on the actions array within the flow definition.
A Studio Flow is a JSON object containing settings, actions, and transitions.
{
"settings": {
"name": "Branching Logic Demo",
"description": "Demonstrates ASSIGN and IF actions"
},
"actions": [],
"transitions": []
}
Step 2: Implementing the ASSIGN Action
The ASSIGN action sets a variable. It is the foundation of state management in Studio. You can assign static values, dynamic inputs (like DTMF digits or IVR input), or expressions.
Key Parameters:
type: Must beassign.name: A unique identifier for this action node in the flow graph.variableName: The name of the variable to create or update.value: The value to assign. This can be a string, number, or an expression referencing other variables.
Example: Assigning a Static Value
Suppose you want to set a variable userTier to “Premium” based on initial data.
const assignAction = {
id: "assign_user_tier",
type: "assign",
name: "Set User Tier",
configuration: {
variableName: "userTier",
value: "Premium"
}
};
Example: Assigning Dynamic Input
In a real IVR, you often assign the result of a GetInput action to a variable. Let us assume a previous action named collect_input captured the user’s choice.
const assignInputAction = {
id: "assign_user_choice",
type: "assign",
name: "Store User Choice",
configuration: {
variableName: "userChoice",
value: "${action.collect_input.input}"
}
};
Note the expression syntax ${action.<action_id>.<field>}. This is critical for referencing outputs from previous steps.
Step 3: Implementing the IF Action for Branching
The IF action evaluates a condition. It has two primary outcomes: true and false. You must define transitions for both to ensure the flow does not hang.
Key Parameters:
type: Must beif.name: Unique identifier for the condition node.condition: An expression that evaluates to a boolean.trueTransition: The ID of the action to execute if the condition is true.falseTransition: The ID of the action to execute if the condition is false.
Example: Simple Equality Check
We want to check if userChoice equals “1”. If yes, go to play_premium_menu. If no, go to play_standard_menu.
const ifAction = {
id: "check_user_choice",
type: "if",
name: "Check User Choice",
configuration: {
condition: "${variable.userChoice} == '1'"
}
};
The condition expression uses standard JavaScript-like syntax for comparison. Supported operators include ==, !=, >, <, >=, <=, &&, ||.
Step 4: Defining Transitions
Actions do not execute in isolation. They are connected via the transitions array. Each transition specifies the from action, the to action, and the event (e.g., true, false, next).
For the IF action above, we need two transitions:
const transitions = [
{
from: "check_user_choice",
to: "play_premium_menu",
event: "true"
},
{
from: "check_user_choice",
to: "play_standard_menu",
event: "false"
}
];
For the ASSIGN action, the default transition is usually next, moving to the subsequent action in the linear sequence.
const assignTransition = {
from: "assign_user_choice",
to: "check_user_choice",
event: "next"
};
Step 5: Assembling the Complete Flow Payload
Now, we combine the actions and transitions into a complete flow definition. We will also include placeholder actions for the targets (play_premium_menu and play_standard_menu) to make the flow valid.
const flowDefinition = {
settings: {
name: "Branching Logic Demo",
description: "Uses ASSIGN and IF to route based on user input"
},
actions: [
{
id: "assign_user_choice",
type: "assign",
name: "Store User Choice",
configuration: {
variableName: "userChoice",
value: "${action.collect_input.input}"
}
},
{
id: "check_user_choice",
type: "if",
name: "Check User Choice",
configuration: {
condition: "${variable.userChoice} == '1'"
}
},
{
id: "play_premium_menu",
type: "play",
name: "Play Premium Menu",
configuration: {
audioUrl: "https://example.com/premium.wav"
}
},
{
id: "play_standard_menu",
type: "play",
name: "Play Standard Menu",
configuration: {
audioUrl: "https://example.com/standard.wav"
}
}
],
transitions: [
{
from: "assign_user_choice",
to: "check_user_choice",
event: "next"
},
{
from: "check_user_choice",
to: "play_premium_menu",
event: "true"
},
{
from: "check_user_choice",
to: "play_standard_menu",
event: "false"
}
]
};
Step 6: Publishing the Flow via API
To apply this logic, you must POST or PUT this definition to the Studio API.
async function updateFlow(token, flowId, flowDefinition) {
const url = `https://${CXONE_DOMAIN}/api/v2/studio/flows/${flowId}`;
try {
const response = await axios.put(url, flowDefinition, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log('Flow updated successfully:', response.data);
return response.data;
} catch (error) {
console.error('Failed to update flow:', error.response?.data || error.message);
throw error;
}
}
// Usage
(async () => {
const token = await getStudioToken();
const flowId = "your-existing-flow-id"; // Replace with actual Flow ID
await updateFlow(token, flowId, flowDefinition);
})();
Complete Working Example
This script demonstrates the full cycle: authenticating, defining a flow with ASSIGN and IF logic, and updating the flow in CXone.
import axios from 'axios';
const CXONE_DOMAIN = 'your-domain.nice-incontact.com';
const CLIENT_ID = process.env.CXONE_CLIENT_ID;
const CLIENT_SECRET = process.env.CXONE_CLIENT_SECRET;
const FLOW_ID = process.env.CXONE_FLOW_ID; // Target Flow ID
// 1. Authentication
async function getStudioToken() {
const url = `https://${CXONE_DOMAIN}/api/v2/oauth/token`;
const params = new URLSearchParams({
grant_type: 'client_credentials',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
scope: 'studio:flows:write studio:flows:read'
});
try {
const response = await axios.post(url, params, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
return response.data.access_token;
} catch (error) {
console.error('Token acquisition failed:', error.response?.data || error.message);
throw error;
}
}
// 2. Flow Definition with ASSIGN and IF
function createBranchingFlow() {
return {
settings: {
name: "API-Generated Branching Flow",
description: "Demonstrates ASSIGN and IF actions"
},
actions: [
// Action 1: Assign a variable from a hypothetical previous input action
{
id: "assign_choice",
type: "assign",
name: "Assign User Choice",
configuration: {
variableName: "userChoice",
value: "${action.previous_input.input}"
}
},
// Action 2: Conditional Logic
{
id: "branch_logic",
type: "if",
name: "Branch on Choice",
configuration: {
condition: "${variable.userChoice} == '1'"
}
},
// Action 3: True Path
{
id: "path_true",
type: "play",
name: "Play True Path Audio",
configuration: {
audioUrl: "https://example.com/true.wav"
}
},
// Action 4: False Path
{
id: "path_false",
type: "play",
name: "Play False Path Audio",
configuration: {
audioUrl: "https://example.com/false.wav"
}
}
],
transitions: [
// Transition from Assign to IF
{
from: "assign_choice",
to: "branch_logic",
event: "next"
},
// Transition from IF to True Path
{
from: "branch_logic",
to: "path_true",
event: "true"
},
// Transition from IF to False Path
{
from: "branch_logic",
to: "path_false",
event: "false"
}
]
};
}
// 3. Update Flow
async function updateFlow(token, flowId, flowData) {
const url = `https://${CXONE_DOMAIN}/api/v2/studio/flows/${flowId}`;
try {
const response = await axios.put(url, flowData, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log('Flow updated successfully.');
return response.data;
} catch (error) {
console.error('Update failed:', error.response?.data || error.message);
throw error;
}
}
// 4. Execution
(async () => {
try {
const token = await getStudioToken();
const flowData = createBranchingFlow();
await updateFlow(token, FLOW_ID, flowData);
} catch (error) {
console.error('Execution failed:', error);
}
})();
Common Errors & Debugging
Error: 400 Bad Request - Invalid Condition Expression
- What causes it: The
conditionfield in theIFaction contains syntax errors or references undefined variables. - How to fix it: Ensure the condition uses valid JavaScript expression syntax. Verify that
${variable.name}matches thevariableNamein theASSIGNaction exactly. Case sensitivity matters. - Code showing the fix:
// Incorrect "condition": "${variable.UserChoice} == '1'" // Variable name mismatch // Correct "condition": "${variable.userChoice} == '1'" // Matches variableName in ASSIGN
Error: 400 Bad Request - Missing Transition
- What causes it: An
IFaction must have bothtrueandfalsetransitions defined in thetransitionsarray. If one is missing, the flow is considered invalid. - How to fix it: Add the missing transition object to the
transitionsarray. - Code showing the fix:
transitions: [ { from: "branch_logic", to: "path_true", event: "true" }, // Missing false transition causes error { from: "branch_logic", to: "path_false", event: "false" } ]
Error: 429 Too Many Requests
- What causes it: Exceeding the rate limit for Studio API calls.
- How to fix it: Implement exponential backoff and retry logic.
- Code showing the fix:
async function retryableRequest(fn, retries = 3) { for (let i = 0; i < retries; i++) { try { return await fn(); } catch (error) { if (error.response?.status === 429) { const delay = Math.pow(2, i) * 1000; console.log(`Rate limited. Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } else { throw error; } } } }
Error: Variable Scope Issues
- What causes it: Variables assigned in one flow segment may not be accessible in another if the flow structure is complex (e.g., across different sub-flows or if the variable was not initialized).
- How to fix it: Ensure the
ASSIGNaction occurs before theIFaction in the execution path. If using sub-flows, verify that variables are passed correctly via context.