How to Automate CXone Studio Flow Branching with ASSIGN and IF Actions via API

How to Automate CXone Studio Flow Branching with ASSIGN and IF Actions via API

What You Will Build

  • You will programmatically create a NICE CXone Studio flow that uses ASSIGN actions to set variables and IF actions to route execution based on those variables.
  • This tutorial uses the NICE CXone Studio API (/api/v2/studio/flows) and the CXone SDK for JavaScript/Node.js.
  • The code is written in JavaScript (ES Modules) using the axios library for HTTP requests, demonstrating the exact JSON structure required for complex branching logic.

Prerequisites

  • OAuth Client: A CXone Application with the studio:flow:write and studio:flow:read scopes.
  • SDK Version: NICE CXone Node.js SDK v2+ or direct REST API access.
  • Runtime: Node.js v18+ or later.
  • Dependencies: axios for HTTP requests, dotenv for environment variable management.
  • Studio Context: An existing Studio Flow ID or the intent to create a new one.

Authentication Setup

Before interacting with the Studio API, you must obtain a valid OAuth 2.0 access token. The Studio API endpoints are protected and require a token with the appropriate scopes. The following code demonstrates a robust authentication helper that handles token retrieval.

import axios from 'axios';
import dotenv from 'dotenv';

dotenv.config();

const { CXONE_BASE_URL, CXONE_CLIENT_ID, CXONE_CLIENT_SECRET } = process.env;

/**
 * Retrieves an OAuth2 access token from NICE CXone.
 * @returns {Promise<string>} The access token.
 */
async function getAccessToken() {
  const authUrl = `${CXONE_BASE_URL}/oauth/token`;
  
  const payload = new URLSearchParams();
  payload.append('grant_type', 'client_credentials');
  payload.append('client_id', CXONE_CLIENT_ID);
  payload.append('client_secret', CXONE_CLIENT_SECRET);

  try {
    const response = await axios.post(authUrl, payload, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    });

    if (response.status !== 200) {
      throw new Error(`Authentication failed with status ${response.status}`);
    }

    return response.data.access_token;
  } catch (error) {
    console.error('Failed to retrieve access token:', error.message);
    throw error;
  }
}

You must store your credentials in a .env file:

CXONE_BASE_URL=https://api.euw1.nice.incontact.com
CXONE_CLIENT_ID=your_client_id_here
CXONE_CLIENT_SECRET=your_client_secret_here

Implementation

Step 1: Define the Flow Skeleton and ASSIGN Action

The first step in building branching logic is to define the flow structure. In CXone Studio, a flow is a directed acyclic graph (DAG) of nodes. We will create a simple flow that starts, assigns a value to a variable, and then branches based on that value.

The ASSIGN action is represented by a node of type assign. It requires a assignments array where each object specifies a variable name and a value. The value can be a literal or an expression.

/**
 * Creates a basic flow skeleton with an ASSIGN node.
 * @param {string} token - The OAuth access token.
 * @returns {Promise<object>} The created flow object.
 */
async function createBaseFlow(token) {
  const flowName = `DevOps Branching Example - ${Date.now()}`;
  const endpoint = `${CXONE_BASE_URL}/api/v2/studio/flows`;

  const flowPayload = {
    name: flowName,
    description: 'Automated flow demonstrating ASSIGN and IF logic.',
    type: 'IVR', // Can be 'IVR', 'SMS', 'Email', etc.
    nodes: [
      {
        id: 'start',
        type: 'start',
        label: 'Start',
        next: 'assign_node', // Directs flow to the ASSIGN node
        properties: {}
      },
      {
        id: 'assign_node',
        type: 'assign',
        label: 'Set Customer Tier',
        next: 'if_node', // Directs flow to the IF node
        properties: {
          assignments: [
            {
              variable: 'customerTier',
              value: 'Premium', // Literal string assignment
              type: 'string'
            }
          ]
        }
      }
    ],
    settings: {
      defaultLanguage: 'en-US'
    }
  };

  try {
    const response = await axios.post(endpoint, flowPayload, {
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      }
    });

    console.log('Base flow created successfully:', response.data.id);
    return response.data;
  } catch (error) {
    if (error.response) {
      console.error('API Error:', error.response.data);
    } else {
      console.error('Request Error:', error.message);
    }
    throw error;
  }
}

Key Parameter Explanation:

  • id: A unique identifier for the node within this flow. It must be unique across all nodes in the nodes array.
  • type: For assignment, this is strictly assign.
  • next: The ID of the next node to execute. In branching scenarios, this is often determined by the IF node, but linear flows use a static ID.
  • assignments: An array of objects. Each object must have variable (the name of the Studio variable), value (the literal or expression), and type (e.g., string, number, boolean).

Step 2: Implement Branching Logic with IF Actions

The IF action is represented by a node of type if. Unlike linear nodes, an IF node has multiple outgoing paths: true and false (and optionally default). You must define the condition using the condition property.

The condition is a string expression that evaluates to a boolean. CXone Studio supports a specific expression language similar to JavaScript but with its own syntax for accessing variables and functions.

/**
 * Updates the flow to include an IF node that branches based on the assigned variable.
 * @param {string} token - The OAuth access token.
 * @param {string} flowId - The ID of the flow to update.
 * @returns {Promise<object>} The updated flow object.
 */
async function addBranchingLogic(token, flowId) {
  const endpoint = `${CXONE_BASE_URL}/api/v2/studio/flows/${flowId}`;

  // First, fetch the existing flow to ensure we do not overwrite other changes
  const fetchResponse = await axios.get(endpoint, {
    headers: { 'Authorization': `Bearer ${token}` }
  });

  const existingFlow = fetchResponse.data;
  const nodes = existingFlow.nodes;

  // Add the IF node
  const ifNode = {
    id: 'if_node',
    type: 'if',
    label: 'Check if Premium',
    // The condition checks if the variable 'customerTier' equals 'Premium'
    condition: "variable('customerTier') == 'Premium'",
    // Define the next node for TRUE and FALSE branches
    true: 'premium_path',
    false: 'standard_path',
    properties: {}
  };

  // Add endpoint nodes for each branch to visualize the result
  const premiumNode = {
    id: 'premium_path',
    type: 'say', // Using 'say' as a placeholder for an action
    label: 'Premium Greeting',
    next: 'end',
    properties: {
      text: 'Welcome, valued Premium member.'
    }
  };

  const standardNode = {
    id: 'standard_path',
    type: 'say',
    label: 'Standard Greeting',
    next: 'end',
    properties: {
      text: 'Welcome to our service.'
    }
  };

  const endNode = {
    id: 'end',
    type: 'end',
    label: 'End Flow',
    properties: {}
  };

  // Update the nodes array in the existing flow
  // We remove the old 'assign_node' next pointer and add the new nodes
  const updatedNodes = nodes.filter(node => node.id !== 'if_node' && node.id !== 'premium_path' && node.id !== 'standard_path' && node.id !== 'end');
  
  // Ensure the assign_node points to the if_node
  const assignNodeIndex = updatedNodes.findIndex(n => n.id === 'assign_node');
  if (assignNodeIndex !== -1) {
    updatedNodes[assignNodeIndex].next = 'if_node';
  }

  // Append new nodes
  updatedNodes.push(ifNode, premiumNode, standardNode, endNode);

  const updatedFlowPayload = {
    ...existingFlow,
    nodes: updatedNodes
  };

  try {
    const response = await axios.put(endpoint, updatedFlowPayload, {
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      }
    });

    console.log('Branching logic added successfully.');
    return response.data;
  } catch (error) {
    if (error.response) {
      console.error('API Error:', error.response.data);
    } else {
      console.error('Request Error:', error.message);
    }
    throw error;
  }
}

Key Parameter Explanation:

  • condition: This is a string containing the Studio Expression Language. To access a variable, use variable('variableName'). To compare, use standard operators like ==, !=, >, <.
  • true: The ID of the node to execute if the condition evaluates to true.
  • false: The ID of the node to execute if the condition evaluates to false.
  • default: Optional. Used if the condition evaluates to null or undefined. If omitted, the flow may throw an error or stop depending on global settings.

Step 3: Handling Complex Expressions and Data Types

Often, you need to branch based on numeric values, dates, or external data. The ASSIGN action can store numbers and booleans. The IF condition must match the data type.

Here is how you modify the ASSIGN action to set a numeric variable and branch based on a threshold.

/**
 * Demonstrates assigning a numeric value and branching on a numeric comparison.
 * @param {string} token - The OAuth access token.
 * @param {string} flowId - The ID of the flow to update.
 */
async function updateWithNumericBranching(token, flowId) {
  const endpoint = `${CXONE_BASE_URL}/api/v2/studio/flows/${flowId}`;

  const fetchResponse = await axios.get(endpoint, {
    headers: { 'Authorization': `Bearer ${token}` }
  });

  let nodes = fetchResponse.data.nodes;

  // Update the existing assign node to set a numeric variable
  const assignNodeIndex = nodes.findIndex(n => n.id === 'assign_node');
  if (assignNodeIndex !== -1) {
    nodes[assignNodeIndex].properties.assignments = [
      {
        variable: 'accountBalance',
        value: '1500', // Stored as string in API, but interpreted as number in expression
        type: 'number'
      }
    ];
  }

  // Find the IF node and update its condition
  const ifNodeIndex = nodes.findIndex(n => n.id === 'if_node');
  if (ifNodeIndex !== -1) {
    // Condition: Check if accountBalance is greater than 1000
    nodes[ifNodeIndex].condition = "number(variable('accountBalance')) > 1000";
    nodes[ifNodeIndex].label = 'Check Balance Threshold';
    
    // Update the labels of the next nodes to reflect the new logic
    const premiumNodeIndex = nodes.findIndex(n => n.id === 'premium_path');
    if (premiumNodeIndex !== -1) {
      nodes[premiumNodeIndex].label = 'High Balance Path';
      nodes[premiumNodeIndex].properties.text = 'Your balance exceeds the threshold.';
    }

    const standardNodeIndex = nodes.findIndex(n => n.id === 'standard_path');
    if (standardNodeIndex !== -1) {
      nodes[standardNodeIndex].label = 'Low Balance Path';
      nodes[standardNodeIndex].properties.text = 'Your balance is within standard limits.';
    }
  }

  try {
    const response = await axios.put(endpoint, {
      ...fetchResponse.data,
      nodes: nodes
    }, {
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      }
    });

    console.log('Numeric branching logic updated.');
  } catch (error) {
    console.error('Failed to update flow:', error.response?.data || error.message);
  }
}

Critical Note on Data Types:
In CXone Studio expressions, variables are often retrieved as strings. To perform numeric comparisons, you must explicitly cast the variable using number(variable('name')). Similarly, use boolean(variable('name')) for boolean logic. Failing to cast can lead to string comparisons (e.g., "1500" > "1000" is true, but "999" > "1000" is false in string comparison, which is correct, but "9" > "10" is true in string comparison, which is incorrect for numbers).

Complete Working Example

The following script combines authentication, flow creation, and branching logic implementation into a single runnable module.

import axios from 'axios';
import dotenv from 'dotenv';

dotenv.config();

const { CXONE_BASE_URL, CXONE_CLIENT_ID, CXONE_CLIENT_SECRET } = process.env;

async function getAccessToken() {
  const authUrl = `${CXONE_BASE_URL}/oauth/token`;
  const payload = new URLSearchParams();
  payload.append('grant_type', 'client_credentials');
  payload.append('client_id', CXONE_CLIENT_ID);
  payload.append('client_secret', CXONE_CLIENT_SECRET);

  try {
    const response = await axios.post(authUrl, payload, {
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
    });
    return response.data.access_token;
  } catch (error) {
    throw new Error(`Authentication failed: ${error.message}`);
  }
}

async function createAndConfigureBranchingFlow() {
  let token;
  try {
    token = await getAccessToken();
    console.log('Authenticated successfully.');
  } catch (error) {
    console.error('Authentication failed. Check your credentials.');
    return;
  }

  const flowName = `DevOps Branching Example - ${Date.now()}`;
  const endpoint = `${CXONE_BASE_URL}/api/v2/studio/flows`;

  const initialFlowPayload = {
    name: flowName,
    description: 'Automated flow demonstrating ASSIGN and IF logic.',
    type: 'IVR',
    nodes: [
      {
        id: 'start',
        type: 'start',
        label: 'Start',
        next: 'assign_node',
        properties: {}
      },
      {
        id: 'assign_node',
        type: 'assign',
        label: 'Set Account Balance',
        next: 'if_node',
        properties: {
          assignments: [
            {
              variable: 'accountBalance',
              value: '1500',
              type: 'number'
            }
          ]
        }
      },
      {
        id: 'if_node',
        type: 'if',
        label: 'Check Balance Threshold',
        condition: "number(variable('accountBalance')) > 1000",
        true: 'high_balance_path',
        false: 'low_balance_path',
        properties: {}
      },
      {
        id: 'high_balance_path',
        type: 'say',
        label: 'High Balance Path',
        next: 'end',
        properties: {
          text: 'Your balance exceeds the threshold.'
        }
      },
      {
        id: 'low_balance_path',
        type: 'say',
        label: 'Low Balance Path',
        next: 'end',
        properties: {
          text: 'Your balance is within standard limits.'
        }
      },
      {
        id: 'end',
        type: 'end',
        label: 'End Flow',
        properties: {}
      }
    ],
    settings: {
      defaultLanguage: 'en-US'
    }
  };

  try {
    const createResponse = await axios.post(endpoint, initialFlowPayload, {
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      }
    });

    console.log(`Flow created successfully with ID: ${createResponse.data.id}`);
    console.log(`Flow Name: ${createResponse.data.name}`);
    
    // Optional: Verify the flow structure
    const verifyResponse = await axios.get(`${endpoint}/${createResponse.data.id}`, {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    
    console.log('Flow verification complete. Nodes count:', verifyResponse.data.nodes.length);

  } catch (error) {
    if (error.response) {
      console.error('API Error:', error.response.status, error.response.data);
    } else {
      console.error('Request Error:', error.message);
    }
  }
}

createAndConfigureBranchingFlow();

Common Errors & Debugging

Error: 400 Bad Request - Invalid Node Structure

Cause: The Studio API is strict about node connectivity. Every node (except end) must have a next property or valid branching paths (true/false). If an IF node references a non-existent ID in its true or false properties, the API rejects the payload.

Fix: Ensure all referenced IDs exist in the nodes array.

// Incorrect: 'non_existent_node' does not exist
{
  id: 'if_node',
  type: 'if',
  condition: "true",
  true: 'non_existent_node', 
  false: 'end'
}

// Correct: 'premium_path' exists in the nodes array
{
  id: 'if_node',
  type: 'if',
  condition: "true",
  true: 'premium_path',
  false: 'end'
}

Error: 400 Bad Request - Expression Evaluation Error

Cause: The condition string in the IF node contains invalid syntax or references a variable that has not been assigned or does not exist in the context.

Fix: Validate the expression syntax. Use variable('name') to access variables. Ensure the variable is assigned before the IF node is reached in the flow execution path.

// Incorrect: Direct variable reference without function
condition: "customerTier == 'Premium'"

// Correct: Using variable() function
condition: "variable('customerTier') == 'Premium'"

Error: 401 Unauthorized

Cause: The access token is expired or missing the studio:flow:write scope.

Fix: Regenerate the token using the getAccessToken() function. Ensure your CXone Application has the correct scopes assigned in the Admin Portal.

Error: 409 Conflict - Flow Already Exists

Cause: Attempting to create a flow with a name that already exists in the same environment.

Fix: Use a unique name, such as appending a timestamp, or update an existing flow using the PUT endpoint instead of POST.

Official References