Implementing Dynamic Branching Logic with ASSIGN and IF Actions in NICE CXone Studio
What You Will Build
- One sentence: This tutorial demonstrates how to construct a data-driven decision engine within a CXone Studio flow using variable assignment and conditional branching.
- One sentence: This uses the NICE CXone Studio Flow API to programmatically define, validate, and deploy a flow containing complex logical structures.
- One sentence: The programming language covered is JavaScript (Node.js) using the
axioslibrary for HTTP requests.
Prerequisites
- OAuth client type: Confidential Client (Client Credentials Grant) with a secret.
- Required OAuth scopes:
flow:write,flow:read,user:read(for tenant context). - SDK/API version: NICE CXone REST API (v1).
- Language/runtime requirements: Node.js 18+ LTS.
- External dependencies:
axios: For HTTP requests.dotenv: For environment variable management.
Install dependencies via npm:
npm install axios dotenv
Authentication Setup
NICE CXone uses OAuth 2.0 Client Credentials flow for server-to-server integration. You must obtain an access token before interacting with the Studio API.
Environment Variables (.env file):
CXONE_TENANT_ID=your_tenant_id
CXONE_CLIENT_ID=your_client_id
CXONE_CLIENT_SECRET=your_client_secret
CXONE_API_BASE_URL=https://api-us-02.nicecxone.com
Authentication Helper (auth.js):
const axios = require('axios');
require('dotenv').config();
class CXoneAuth {
constructor() {
this.tenantId = process.env.CXONE_TENANT_ID;
this.clientId = process.env.CXONE_CLIENT_ID;
this.clientSecret = process.env.CXONE_CLIENT_SECRET;
this.baseUrl = process.env.CXONE_API_BASE_URL;
this.token = null;
this.tokenExpiry = 0;
}
async getToken() {
if (this.token && Date.now() < this.tokenExpiry) {
return this.token;
}
const url = `${this.baseUrl}/api/v2/authorize/token`;
const params = new URLSearchParams();
params.append('grant_type', 'client_credentials');
params.append('client_id', this.clientId);
params.append('client_secret', this.clientSecret);
try {
const response = await axios.post(url, params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')}`
}
});
this.token = response.data.access_token;
// Tokens expire in 3600 seconds (1 hour). Subtract 60s for buffer.
this.tokenExpiry = Date.now() + (response.data.expires_in - 60) * 1000;
return this.token;
} catch (error) {
console.error('Failed to obtain OAuth token:', error.response?.data || error.message);
throw new Error('Authentication failed');
}
}
async getHeaders() {
const token = await this.getToken();
return {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Request-ID': crypto.randomUUID() // Good practice for tracing
};
}
}
module.exports = new CXoneAuth();
Implementation
Step 1: Define the Flow Structure and ASSIGN Actions
In CXone Studio, logic does not exist in a vacuum. It requires a context. We will create a flow that evaluates an incoming variable. First, we must define the flow container and the ASSIGN actions that prepare the data for evaluation.
The ASSIGN action is critical because it allows you to transform raw input (e.g., a JSON body from an API call or a specific attribute from the interaction) into a clean, typed variable that the IF action can evaluate.
Flow Payload Construction:
We will construct the flow definition in memory. A Studio flow is a directed acyclic graph (DAG). We need to define:
- Start Node: The entry point.
- Assign Node: Sets a variable
calculationResult. - Condition Node (IF): Checks the value of
calculationResult. - End Nodes: The outcomes of the branch.
const createBranchingFlow = () => {
const flowId = crypto.randomUUID();
const tenantId = process.env.CXONE_TENANT_ID;
// 1. Define the Start Node
const startNode = {
id: 'start',
type: 'start',
configuration: {
type: 'api' // Assuming this flow is triggered via API
},
edges: [{
target: 'assign_calculation'
}]
};
// 2. Define the ASSIGN Action
// This action takes the incoming 'score' from the request body
// and assigns it to a local variable 'finalScore'.
// It also performs a simple transformation: multiply by 10.
const assignNode = {
id: 'assign_calculation',
type: 'assign',
configuration: {
assignments: [
{
variable: 'finalScore',
value: {
type: 'expression',
expression: 'request.body.score * 10'
},
dataType: 'number'
},
{
variable: 'timestamp',
value: {
type: 'expression',
expression: 'now()'
},
dataType: 'string'
}
]
},
edges: [{
target: 'check_score'
}]
};
// 3. Define the IF (Condition) Action
// This node evaluates 'finalScore > 50'
const conditionNode = {
id: 'check_score',
type: 'condition',
configuration: {
conditions: [
{
expression: 'finalScore > 50',
edges: [{ target: 'high_score_path' }]
}
],
defaultEdges: [{ target: 'low_score_path' }]
}
};
// 4. Define End Nodes (Outcomes)
const highScoreEnd = {
id: 'high_score_path',
type: 'end',
configuration: {
response: {
status: 200,
body: {
message: 'Score is high',
value: 'finalScore'
}
}
}
};
const lowScoreEnd = {
id: 'low_score_path',
type: 'end',
configuration: {
response: {
status: 200,
body: {
message: 'Score is low',
value: 'finalScore'
}
}
}
};
// Combine into the final Flow Object
return {
id: flowId,
tenantId: tenantId,
name: 'Dynamic Branching Logic Demo',
description: 'Demonstrates ASSIGN and IF actions',
nodes: [startNode, assignNode, conditionNode, highScoreEnd, lowScoreEnd],
edges: [] // Edges are embedded in nodes for this API version, but explicit edge lists may be required depending on specific endpoint version.
// Note: The CXone Studio API often expects edges to be defined within the node objects as shown above for simplicity in creation.
};
};
Key Technical Details:
request.body.score: This assumes the triggering API call sends a JSON payload with ascorefield.expression: The CXone expression language supports basic arithmetic, string manipulation, and date functions.dataType: Explicitly typing variables prevents runtime errors in theIFcondition. IffinalScorewas a string,> 50might perform lexicographical comparison rather than numerical.
Step 2: Create the Flow via API
Now that we have the structure, we send it to the CXone API. The endpoint for creating a flow is POST /api/v2/studio/flows.
const axios = require('axios');
const auth = require('./auth');
async function createFlow() {
try {
const headers = await auth.getHeaders();
const flowDefinition = createBranchingFlow();
const url = `${process.env.CXONE_API_BASE_URL}/api/v2/studio/flows`;
console.log('Creating flow:', flowDefinition.name);
const response = await axios.post(url, flowDefinition, { headers });
console.log('Flow Created Successfully');
console.log('Flow ID:', response.data.id);
console.log('Flow Version:', response.data.version);
return response.data;
} catch (error) {
if (error.response) {
console.error('API Error Status:', error.response.status);
console.error('API Error Data:', error.response.data);
// Handle specific CXone errors
if (error.response.status === 409) {
console.error('Conflict: Flow ID might already exist or invalid structure.');
} else if (error.response.status === 400) {
console.error('Bad Request: Check the flow JSON structure and expressions.');
}
} else {
console.error('Network Error:', error.message);
}
throw error;
}
}
// Execute
createFlow().catch(console.error);
Expected Response:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenantId": "your_tenant_id",
"name": "Dynamic Branching Logic Demo",
"version": 1,
"state": "DRAFT",
"createdAt": "2023-10-27T10:00:00.000Z",
"modifiedAt": "2023-10-27T10:00:00.000Z"
}
Step 3: Publish and Validate the Flow
A flow in DRAFT state cannot be executed. You must publish it. Before publishing, it is best practice to validate the flow structure to catch syntax errors in expressions.
Validation Endpoint: POST /api/v2/studio/flows/{flowId}/validate
async function validateAndPublishFlow(flowId) {
const headers = await auth.getHeaders();
const baseUrl = process.env.CXONE_API_BASE_URL;
// Step 3a: Validate
const validateUrl = `${baseUrl}/api/v2/studio/flows/${flowId}/validate`;
try {
console.log('Validating flow...');
const validateResponse = await axios.post(validateUrl, {}, { headers });
if (validateResponse.data.valid) {
console.log('Validation passed.');
} else {
console.error('Validation failed with issues:');
validateResponse.data.issues.forEach(issue => {
console.error(`- ${issue.message} at ${issue.path}`);
});
throw new Error('Flow validation failed');
}
// Step 3b: Publish
console.log('Publishing flow...');
const publishUrl = `${baseUrl}/api/v2/studio/flows/${flowId}/publish`;
const publishResponse = await axios.post(publishUrl, {}, { headers });
console.log('Flow Published Successfully');
console.log('Published Version:', publishResponse.data.version);
return publishResponse.data;
} catch (error) {
if (error.response) {
console.error('Publish/Validate Error:', error.response.data);
}
throw error;
}
}
Step 4: Complex Branching with Nested IF and Multiple ASSIGNS
Real-world scenarios rarely involve a single boolean check. Often, you need to assign multiple variables based on previous assignments and branch on multiple conditions.
Here is how to structure a Nested IF scenario.
Scenario:
- Assign
userTierbased onaccountBalance. - Assign
isEligiblebased onuserTierandage. - Branch:
- If
isEligibleis true ANDuserTieris ‘Gold’, go toPremiumSupport. - If
isEligibleis true ANDuserTieris ‘Silver’, go toStandardSupport. - Else, go to
DenyAccess.
- If
Updated Node Definitions:
const createComplexFlow = () => {
const flowId = crypto.randomUUID();
const tenantId = process.env.CXONE_TENANT_ID;
const startNode = {
id: 'start',
type: 'start',
configuration: { type: 'api' },
edges: [{ target: 'assign_tier' }]
};
// Assign Tier
const assignTierNode = {
id: 'assign_tier',
type: 'assign',
configuration: {
assignments: [
{
variable: 'userTier',
value: {
type: 'expression',
// Ternary logic in CXone expression
expression: 'request.body.accountBalance > 10000 ? "Gold" : "Silver"'
},
dataType: 'string'
}
]
},
edges: [{ target: 'assign_eligibility' }]
};
// Assign Eligibility
const assignEligibilityNode = {
id: 'assign_eligibility',
type: 'assign',
configuration: {
assignments: [
{
variable: 'isEligible',
value: {
type: 'expression',
expression: 'request.body.age >= 18'
},
dataType: 'boolean'
}
]
},
edges: [{ target: 'check_tier_and_eligibility' }]
};
// Complex IF Condition
const conditionNode = {
id: 'check_tier_and_eligibility',
type: 'condition',
configuration: {
conditions: [
{
// First branch: Gold and Eligible
expression: 'isEligible && userTier == "Gold"',
edges: [{ target: 'premium_support' }]
},
{
// Second branch: Silver and Eligible
expression: 'isEligible && userTier == "Silver"',
edges: [{ target: 'standard_support' }]
}
],
defaultEdges: [{ target: 'deny_access' }]
}
};
// End Nodes
const premiumEnd = {
id: 'premium_support',
type: 'end',
configuration: {
response: {
status: 200,
body: { outcome: 'Premium Support Route' }
}
}
};
const standardEnd = {
id: 'standard_support',
type: 'end',
configuration: {
response: {
status: 200,
body: { outcome: 'Standard Support Route' }
}
}
};
const denyEnd = {
id: 'deny_access',
type: 'end',
configuration: {
response: {
status: 403,
body: { outcome: 'Access Denied' }
}
}
};
return {
id: flowId,
tenantId: tenantId,
name: 'Complex Branching Logic',
nodes: [startNode, assignTierNode, assignEligibilityNode, conditionNode, premiumEnd, standardEnd, denyEnd]
};
};
Why this structure matters:
- Separation of Concerns: The
ASSIGNnodes handle data transformation and derivation. TheIFnode handles pure logic. This makes debugging easier. If the wrong branch is taken, you can inspect the values ofuserTierandisEligiblein the flow logs without guessing if the expression was malformed. - Readability: Using named variables (
userTier) in theIFexpression is far more maintainable than embedding the entirerequest.body.accountBalance > 10000logic directly in the condition.
Complete Working Example
Combine the authentication, flow creation, and validation into a single executable script.
main.js:
const axios = require('axios');
require('dotenv').config();
const auth = require('./auth');
// Import the flow creation functions from previous steps
// (In a real app, these would be in separate modules)
const createBranchingFlow = require('./flowBuilder').createBranchingFlow;
const createComplexFlow = require('./flowBuilder').createComplexFlow;
async function main() {
try {
// 1. Choose which flow to build
const flowDefinition = createComplexFlow();
// 2. Create the Flow
const headers = await auth.getHeaders();
const createUrl = `${process.env.CXONE_API_BASE_URL}/api/v2/studio/flows`;
const createResponse = await axios.post(createUrl, flowDefinition, { headers });
const flowId = createResponse.data.id;
console.log(`Created Flow ID: ${flowId}`);
// 3. Validate the Flow
const validateUrl = `${process.env.CXONE_API_BASE_URL}/api/v2/studio/flows/${flowId}/validate`;
const validateResponse = await axios.post(validateUrl, {}, { headers });
if (!validateResponse.data.valid) {
console.error('Validation failed:', validateResponse.data.issues);
return;
}
console.log('Flow validated successfully.');
// 4. Publish the Flow
const publishUrl = `${process.env.CXONE_API_BASE_URL}/api/v2/studio/flows/${flowId}/publish`;
await axios.post(publishUrl, {}, { headers });
console.log('Flow published successfully.');
console.log('You can now trigger this flow via API using Flow ID:', flowId);
} catch (error) {
console.error('Execution failed:', error.message);
if (error.response) {
console.error('Response Data:', error.response.data);
}
}
}
main();
flowBuilder.js:
const crypto = require('crypto');
const createComplexFlow = () => {
// ... (Insert the createComplexFlow function from Step 4 here)
// Ensure to export it:
return {
id: crypto.randomUUID(),
tenantId: process.env.CXONE_TENANT_ID,
name: 'Complex Branching Logic',
nodes: [
// ... nodes defined in Step 4
]
};
};
module.exports = { createComplexFlow };
Common Errors & Debugging
Error: 400 Bad Request - “Invalid Expression”
- Cause: The expression in the
ASSIGNorIFnode is syntactically incorrect. Common issues include using JavaScript-style==instead of===(though CXone often accepts==), referencing undefined variables, or incorrect date formatting. - Fix: Check the
issuesarray in the validation response. It provides the exact path (e.g.,nodes[2].configuration.conditions[0].expression) and the error message. - Code Fix:
// Incorrect "expression": "request.body.score > 50 && request.body.status == 'active'" // Correct (Ensure variable names match exactly, case-sensitive) "expression": "request.body.score > 50 && request.body.status === 'active'"
Error: 403 Forbidden - “Access Denied”
- Cause: The OAuth token lacks the
flow:writescope, or the client ID is not authorized to manage Studio flows. - Fix: Verify the scopes in your OAuth client configuration in the CXone Admin Console. Ensure the token used in
auth.jsincludesflow:write.
Error: 409 Conflict - “Flow Already Exists”
- Cause: You are trying to create a flow with an ID that already exists in the tenant.
- Fix: Use
crypto.randomUUID()for the flow ID, or retrieve the existing flow ID and update it usingPUT /api/v2/studio/flows/{flowId}instead ofPOST.
Error: Flow Executes but Returns Wrong Branch
- Cause: Data type mismatch. For example,
request.body.scoreis a string"60"but the comparison> 50is treating it as a string or the assignment didn’t cast it to a number. - Fix: Explicitly set
dataType: 'number'in theASSIGNaction and useparseFloatorparseIntin the expression if necessary."value": { "type": "expression", "expression": "parseInt(request.body.score)" }