Increase Data Action Timeout from 3s to 30s in NICE CXone Studio

Increase Data Action Timeout from 3s to 30s in NICE CXone Studio

What You Will Build

  • You will build a Node.js script that identifies a specific Studio Flow with a timing-out Data Action and updates its configuration to extend the timeout limit to 30 seconds.
  • This tutorial uses the NICE CXone REST API, specifically the Flow Management and Data Action endpoints.
  • The implementation uses JavaScript (Node.js) with the axios library for HTTP requests.

Prerequisites

  • OAuth Client Type: Server-to-Server (Client Credentials) or User-to-Server. Server-to-Server is preferred for automated configuration management.
  • Required Scopes:
    • flows:write (To update the Flow configuration)
    • flows:read (To retrieve the current Flow definition)
    • dataActions:write (If modifying the Data Action definition itself, though often the timeout is a Flow-level node property)
  • Runtime: Node.js 16+
  • External Dependencies: axios (npm install axios)
  • Environment Variables:
    • CXONE_TENANT: Your CXone tenant ID (e.g., mytenant)
    • CXONE_API_HOST: Usually api.nicecxone.com or regional equivalent (e.g., api-eu.nicecxone.com)
    • CXONE_CLIENT_ID: OAuth Client ID
    • CXONE_CLIENT_SECRET: OAuth Client Secret

Authentication Setup

NICE CXone uses OAuth 2.0 for authentication. Before making any API calls, you must obtain an access token. The following code demonstrates a robust way to handle token acquisition and caching to avoid unnecessary re-authentication.

const axios = require('axios');

class CXoneAuth {
  constructor(tenant, clientId, clientSecret) {
    this.tenant = tenant;
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.baseUrl = `https://${tenant}.api.nicecxone.com`;
    this.token = null;
    this.tokenExpiry = 0;
  }

  async getAccessToken() {
    // Return cached token if still valid (with 60s buffer)
    if (this.token && Date.now() < this.tokenExpiry - 60000) {
      return this.token;
    }

    try {
      const response = await axios.post(`${this.baseUrl}/oauth/token`, {
        grant_type: 'client_credentials'
      }, {
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Basic ' + Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')
        }
      });

      this.token = response.data.access_token;
      // Set expiry to now + expires_in seconds
      this.tokenExpiry = Date.now() + (response.data.expires_in * 1000);
      return this.token;
    } catch (error) {
      if (error.response) {
        throw new Error(`Auth Failed: ${error.response.status} - ${error.response.data.message}`);
      }
      throw error;
    }
  }

  getHeaders() {
    return async () => {
      const token = await this.getAccessToken();
      return {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      };
    };
  }
}

// Initialize Auth
const auth = new CXoneAuth(
  process.env.CXONE_TENANT,
  process.env.CXONE_CLIENT_ID,
  process.env.CXONE_CLIENT_SECRET
);

Implementation

Step 1: Retrieve the Target Flow Definition

To modify a timeout, you first need the current state of the Flow. You cannot patch a node directly without knowing its unique identifier within the flow graph. We will fetch the flow by its ID.

Note: The default timeout for many Data Actions in CXone Studio is indeed low (often 3-5 seconds depending on the action type). Increasing this requires modifying the node properties in the Flow definition.

async function getFlowDefinition(flowId) {
  const headers = await auth.getHeaders();
  const url = `${auth.baseUrl}/api/v2/flows/${flowId}`;

  try {
    const response = await axios.get(url, { headers });
    
    // Validate response structure
    if (!response.data || !response.data.nodes) {
      throw new Error('Invalid Flow structure: missing nodes array');
    }
    
    return response.data;
  } catch (error) {
    if (error.response && error.response.status === 404) {
      throw new Error(`Flow with ID ${flowId} not found.`);
    }
    if (error.response && error.response.status === 401) {
      throw new Error('Unauthorized: Check OAuth token and scopes.');
    }
    throw error;
  }
}

Step 2: Locate and Update the Data Action Node

The core logic involves traversing the nodes array in the Flow definition to find the specific Data Action. In CXone Studio API responses, each node has a type and a properties object.

For a “Data Action” node (often represented as type: "dataAction" or specific action types like httpRequest, webService, etc.), the timeout is usually found in the properties object under a key like timeout, executionTimeout, or httpTimeout.

Critical Note on Parameter Names:

  • For HTTP Request actions: The property is often httpTimeout or timeout.
  • For Custom Data Actions: The timeout might be defined in the Action Definition itself, but the Flow node can sometimes override it via executionTimeout.
  • If the timeout is hardcoded in the Action Definition (not the Flow node), you must update the Action Definition using /api/v2/data-actions/{id}. However, most transient timeouts are Flow-node specific.

We will assume the standard case: updating the Flow node property.

function updateNodeTimeout(flowData, targetNodeId, newTimeoutMs) {
  const nodes = flowData.nodes;
  let updatedNode = null;

  // Find the node by ID
  const nodeIndex = nodes.findIndex(node => node.id === targetNodeId);

  if (nodeIndex === -1) {
    throw new Error(`Node with ID ${targetNodeId} not found in Flow.`);
  }

  const node = nodes[nodeIndex];

  // Determine the correct property name based on node type
  // Common types: 'httpRequest', 'dataAction', 'webService'
  let timeoutProperty = null;

  if (node.type === 'httpRequest' || node.type === 'webService') {
    timeoutProperty = 'httpTimeout';
  } else if (node.type === 'dataAction') {
    // Some generic data actions use 'executionTimeout'
    timeoutProperty = 'executionTimeout';
  } else {
    // Fallback: check if 'timeout' exists in properties
    if (node.properties && node.properties.hasOwnProperty('timeout')) {
      timeoutProperty = 'timeout';
    } else {
      throw new Error(`Unknown node type '${node.type}' or timeout property not identified.`);
    }
  }

  // Ensure properties object exists
  if (!node.properties) {
    node.properties = {};
  }

  // Update the timeout value
  // Note: CXone often expects milliseconds. 30 seconds = 30000 ms.
  node.properties[timeoutProperty] = newTimeoutMs;
  
  updatedNode = node;
  console.log(`Updated node ${targetNodeId} property '${timeoutProperty}' to ${newTimeoutMs}ms`);
  
  return flowData;
}

Step 3: Publish the Updated Flow

Modifying the JSON definition locally does not change the live environment. You must PUT the updated definition back to the API. Crucially, you must also handle the version field if the API requires optimistic locking, or simply send the full object. CXone Flow API typically expects the full flow definition including metadata.

async function publishFlow(flowId, flowData) {
  const headers = await auth.getHeaders();
  const url = `${auth.baseUrl}/api/v2/flows/${flowId}`;

  try {
    // CXone Flow API requires the full object. 
    // Ensure we are not stripping out critical metadata like 'id', 'name', 'version'
    const response = await axios.put(url, flowData, { headers });
    
    console.log('Flow updated successfully.');
    return response.data;
  } catch (error) {
    if (error.response && error.response.status === 409) {
      throw new Error('Conflict: Flow version mismatch. Fetch the latest version and retry.');
    }
    if (error.response && error.response.status === 400) {
      throw new Error(`Bad Request: ${JSON.stringify(error.response.data)}`);
    }
    throw error;
  }
}

Complete Working Example

This script combines all steps. It authenticates, fetches a flow, finds a specific node (by ID), increases its timeout to 30 seconds (30,000 ms), and saves the changes.

const axios = require('axios');

// --- Configuration ---
const FLOW_ID = process.env.CXONE_FLOW_ID || 'YOUR_FLOW_ID_HERE';
const NODE_ID = process.env.CXONE_NODE_ID || 'YOUR_NODE_ID_HERE'; // The specific data action node ID
const NEW_TIMEOUT_MS = 30000; // 30 seconds

// --- Auth Class (from Step 1) ---
class CXoneAuth {
  constructor(tenant, clientId, clientSecret) {
    this.tenant = tenant;
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.baseUrl = `https://${tenant}.api.nicecxone.com`;
    this.token = null;
    this.tokenExpiry = 0;
  }

  async getAccessToken() {
    if (this.token && Date.now() < this.tokenExpiry - 60000) {
      return this.token;
    }
    try {
      const response = await axios.post(`${this.baseUrl}/oauth/token`, {
        grant_type: 'client_credentials'
      }, {
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Basic ' + Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')
        }
      });
      this.token = response.data.access_token;
      this.tokenExpiry = Date.now() + (response.data.expires_in * 1000);
      return this.token;
    } catch (error) {
      throw new Error(`Auth Failed: ${error.response?.data?.message || error.message}`);
    }
  }

  async getHeaders() {
    const token = await this.getAccessToken();
    return {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    };
  }
}

// --- Main Execution Logic ---

async function main() {
  // 1. Initialize Authentication
  const auth = new CXoneAuth(
    process.env.CXONE_TENANT,
    process.env.CXONE_CLIENT_ID,
    process.env.CXONE_CLIENT_SECRET
  );

  console.log('Fetching Flow Definition...');
  
  // 2. Get Flow Definition
  const headers = await auth.getHeaders();
  const flowUrl = `${auth.baseUrl}/api/v2/flows/${FLOW_ID}`;
  
  let flowData;
  try {
    const flowResponse = await axios.get(flowUrl, { headers });
    flowData = flowResponse.data;
  } catch (err) {
    console.error('Failed to fetch flow:', err.message);
    process.exit(1);
  }

  // 3. Locate and Update Node
  const nodes = flowData.nodes;
  const targetNode = nodes.find(n => n.id === NODE_ID);

  if (!targetNode) {
    console.error(`Node ID ${NODE_ID} not found in Flow ${FLOW_ID}`);
    console.log('Available Node IDs:', nodes.map(n => n.id).join(', '));
    process.exit(1);
  }

  console.log(`Found Node: ${targetNode.id} (Type: ${targetNode.type})`);

  // Determine property name based on type
  let timeoutProp = 'timeout'; // Default fallback
  
  if (targetNode.type === 'httpRequest') {
    timeoutProp = 'httpTimeout';
  } else if (targetNode.type === 'dataAction') {
    timeoutProp = 'executionTimeout';
  }

  // Initialize properties if missing
  if (!targetNode.properties) {
    targetNode.properties = {};
  }

  // Check current value
  const currentTimeout = targetNode.properties[timeoutProp];
  console.log(`Current Timeout: ${currentTimeout ? currentTimeout + 'ms' : 'Not set'}`);

  // Update Timeout
  targetNode.properties[timeoutProp] = NEW_TIMEOUT_MS;
  console.log(`Setting Timeout to: ${NEW_TIMEOUT_MS}ms`);

  // 4. Publish Changes
  console.log('Publishing updated Flow...');
  try {
    await axios.put(flowUrl, flowData, { headers });
    console.log('Success: Flow updated and published.');
  } catch (err) {
    console.error('Failed to publish flow:', err.message);
    if (err.response) {
      console.error('Response Data:', err.response.data);
    }
    process.exit(1);
  }
}

// Run
main().catch(console.error);

Common Errors & Debugging

Error: 400 Bad Request - “Invalid property value”

  • Cause: The timeout value is outside the allowed range. CXone typically enforces a minimum of 1000ms (1s) and a maximum of 30000ms (30s) or sometimes 60000ms (60s) depending on the action type. Some HTTP actions allow up to 120s.
  • Fix: Verify the NEW_TIMEOUT_MS value. Ensure it is an integer. If you need >30s, check if the specific action type supports it. For standard Data Actions, 30s is often the hard cap. If you need longer, consider breaking the action into asynchronous steps with callbacks.

Error: 409 Conflict - “Version mismatch”

  • Cause: The Flow was modified by another user or process after you fetched it but before you attempted to update it. CXone uses optimistic locking.
  • Fix: Implement a retry loop. Fetch the latest version, apply your change again, and retry the PUT request.
// Example Retry Logic for 409
async function updateWithRetry(flowId, flowData, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const headers = await auth.getHeaders();
      await axios.put(`${auth.baseUrl}/api/v2/flows/${flowId}`, flowData, { headers });
      return true;
    } catch (error) {
      if (error.response && error.response.status === 409) {
        console.log(`Conflict detected. Refreshing flow data... (Attempt ${i + 1})`);
        // Re-fetch the latest version
        const freshFlow = await axios.get(`${auth.baseUrl}/api/v2/flows/${flowId}`, { headers: await auth.getHeaders() });
        // Re-apply the timeout change to the fresh object
        // ... (repeat node update logic) ...
        flowData = freshFlow.data; // Update the local object reference
        continue; 
      }
      throw error; // Re-throw if not a 409
    }
  }
  throw new Error('Max retries exceeded for 409 Conflict');
}

Error: 403 Forbidden - “Insufficient permissions”

  • Cause: The OAuth token lacks the flows:write scope.
  • Fix: Regenerate the OAuth token using a client credential set that includes flows:write. Check your CXone Admin Console > Settings > Integrations > OAuth Clients.

Error: Timeout property not taking effect

  • Cause: You updated the wrong property name.
  • Fix: Inspect the raw JSON response from GET /api/v2/flows/{id}. Look at the properties object of the target node. Identify the exact key name used for timeout (e.g., timeout, httpTimeout, executionTimeout). The code above attempts to guess based on type, but custom actions may vary.

Official References