Implementing Dynamic Branching Logic with ASSIGN and IF Actions in NICE CXone Studio

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 axios library 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:

  1. Start Node: The entry point.
  2. Assign Node: Sets a variable calculationResult.
  3. Condition Node (IF): Checks the value of calculationResult.
  4. 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 a score field.
  • expression: The CXone expression language supports basic arithmetic, string manipulation, and date functions.
  • dataType: Explicitly typing variables prevents runtime errors in the IF condition. If finalScore was a string, > 50 might 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:

  1. Assign userTier based on accountBalance.
  2. Assign isEligible based on userTier and age.
  3. Branch:
    • If isEligible is true AND userTier is ‘Gold’, go to PremiumSupport.
    • If isEligible is true AND userTier is ‘Silver’, go to StandardSupport.
    • Else, go to DenyAccess.

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 ASSIGN nodes handle data transformation and derivation. The IF node handles pure logic. This makes debugging easier. If the wrong branch is taken, you can inspect the values of userTier and isEligible in the flow logs without guessing if the expression was malformed.
  • Readability: Using named variables (userTier) in the IF expression is far more maintainable than embedding the entire request.body.accountBalance > 10000 logic 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 ASSIGN or IF node 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 issues array 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:write scope, 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.js includes flow: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 using PUT /api/v2/studio/flows/{flowId} instead of POST.

Error: Flow Executes but Returns Wrong Branch

  • Cause: Data type mismatch. For example, request.body.score is a string "60" but the comparison > 50 is treating it as a string or the assignment didn’t cast it to a number.
  • Fix: Explicitly set dataType: 'number' in the ASSIGN action and use parseFloat or parseInt in the expression if necessary.
    "value": {
      "type": "expression",
      "expression": "parseInt(request.body.score)"
    }
    

Official References