Automating NICE CXone Agent Assist Task Creation with Node.js

Automating NICE CXone Agent Assist Task Creation with Node.js

What You Will Build

A Node.js Express service that receives interaction completion webhooks, evaluates post-interaction form responses, generates prioritized tasks via the Workflow API, and pushes desktop notifications to assigned agents. This tutorial uses the NICE CXone REST API v2 with axios and express. The programming language covered is JavaScript (Node.js).

Prerequisites

  • OAuth Client Credentials grant type with scopes: interaction:read, postinteractionform:read, workflow:task:create, notification:write
  • CXone API v2 endpoints
  • Node.js 18 or higher
  • External dependencies: express, axios, dotenv

Authentication Setup

CXone uses OAuth 2.0 Client Credentials flow. The service must cache the access token and request a new one when the previous token expires. The token endpoint is POST https://{org}.api.cxone.com/oauth/token.

const axios = require('axios');
require('dotenv').config();

const CXONE_ORG = process.env.CXONE_ORG;
const CXONE_CLIENT_ID = process.env.CXONE_CLIENT_ID;
const CXONE_CLIENT_SECRET = process.env.CXONE_CLIENT_SECRET;
const BASE_URL = `https://${CXONE_ORG}.api.cxone.com`;

let cachedToken = null;
let tokenExpiry = 0;

async function getAccessToken() {
  if (cachedToken && Date.now() < tokenExpiry) {
    return cachedToken;
  }

  const payload = new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: CXONE_CLIENT_ID,
    client_secret: CXONE_CLIENT_SECRET,
    scope: 'interaction:read postinteractionform:read workflow:task:create notification:write'
  });

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

    cachedToken = response.data.access_token;
    tokenExpiry = Date.now() + (response.data.expires_in * 1000) - 30000; // Refresh 30s early
    return cachedToken;
  } catch (error) {
    if (error.response) {
      throw new Error(`OAuth failed with status ${error.response.status}: ${error.response.data}`);
    }
    throw error;
  }
}

The getAccessToken function manages token lifecycle. It checks expiration, performs the client credentials exchange, and stores the token with a safety margin. All subsequent API calls will call this function to attach the Authorization: Bearer <token> header.

Implementation

Step 1: Receive Interaction Completion Events

CXone Eventing delivers interaction.completed payloads to a registered webhook URL. The service exposes an Express route to capture the event and extract the interactionId and agentId.

const express = require('express');
const app = express();
app.use(express.json());

app.post('/webhooks/interaction-completed', async (req, res) => {
  const event = req.body;
  
  // Validate event structure
  if (!event.interactionId || !event.agentId) {
    console.error('Missing interactionId or agentId in event payload');
    return res.status(400).json({ error: 'Invalid event payload' });
  }

  console.log(`Received interaction.completed for ID: ${event.interactionId}`);
  res.status(200).json({ received: true });

  // Async processing to avoid blocking the webhook response
  processInteractionCompletion(event.interactionId, event.agentId).catch(console.error);
});

The webhook responds immediately with 200 OK to prevent CXone Eventing from retrying. The actual processing runs asynchronously via processInteractionCompletion. Required scope for downstream calls: interaction:read.

Step 2: Evaluate Post-Interaction Forms

The service fetches post-interaction form responses using the Interaction API. It evaluates a specific field to determine task priority and due date.

async function fetchPostInteractionForm(interactionId) {
  const token = await getAccessToken();
  const endpoint = `${BASE_URL}/api/v2/interactions/${interactionId}/postinteractionforms`;

  try {
    const response = await axios.get(endpoint, {
      headers: { Authorization: `Bearer ${token}` },
      validateStatus: (status) => status === 200 || status === 404
    });

    if (response.status === 404) {
      console.log('No post-interaction form found for this interaction');
      return null;
    }

    return response.data;
  } catch (error) {
    console.error('Failed to fetch post-interaction form:', error.message);
    throw error;
  }
}

function evaluateFormResponses(formData) {
  if (!formData || !formData.responses) {
    return { createTask: false };
  }

  // Example: Check satisfaction rating field
  const satisfactionResponse = formData.responses.find(r => r.fieldName === 'overall_satisfaction');
  const satisfactionScore = satisfactionResponse ? parseFloat(satisfactionResponse.value) : 5;

  let priority = 'Normal';
  let dueDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // 7 days

  if (satisfactionScore < 3) {
    priority = 'High';
    dueDate = new Date(Date.now() + 1 * 24 * 60 * 60 * 1000).toISOString(); // 1 day
  } else if (satisfactionScore < 4) {
    priority = 'Medium';
    dueDate = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(); // 3 days
  }

  return {
    createTask: satisfactionScore < 4,
    priority,
    dueDate,
    satisfactionScore
  };
}

The evaluateFormResponses function maps satisfaction scores to priority levels and due dates. Scores below 4 trigger task creation. The dueDate field must be ISO 8601 formatted for the Workflow API. Required scope: postinteractionform:read.

Step 3: Create Tasks and Link to Interaction IDs

The Workflow API creates tasks with custom fields. The interaction ID is stored in a custom field for traceability. The service implements retry logic for 429 rate limits.

async function createWorkflowTask(interactionId, agentId, priority, dueDate, satisfactionScore) {
  const token = await getAccessToken();
  const endpoint = `${BASE_URL}/api/v2/workflows/tasks`;

  const taskPayload = {
    subject: `Agent Assist: Follow-up required for interaction ${interactionId}`,
    description: `Post-interaction satisfaction score: ${satisfactionScore}. Requires agent review and remediation.`,
    dueDate: dueDate,
    priority: priority,
    assigneeId: agentId,
    customFields: [
      {
        id: 'interaction_id_link', // Replace with your actual custom field ID from CXone
        value: interactionId
      }
    ]
  };

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

    console.log('Task created successfully:', response.data.id);
    return response.data;
  } catch (error) {
    console.error('Failed to create workflow task:', error.message);
    throw error;
  }
}

// Axios instance with 429 retry logic
const axiosWithRetry = axios.create();
axiosWithRetry.interceptors.response.use(
  response => response,
  async error => {
    const originalConfig = error.config;
    if (!originalConfig) throw error;

    if (error.response?.status === 429 && !originalConfig._retryCount) {
      originalConfig._retryCount = originalConfig._retryCount || 0;
      if (originalConfig._retryCount < 3) {
        originalConfig._retryCount++;
        const retryAfter = error.response.headers['retry-after'] || Math.pow(2, originalConfig._retryCount);
        console.log(`Rate limited. Retrying in ${retryAfter} seconds...`);
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
        return axiosWithRetry(originalConfig);
      }
    }
    return Promise.reject(error);
  }
);

The axiosWithRetry interceptor catches 429 responses, parses the Retry-After header, and applies exponential backoff up to three attempts. The task payload includes a custom field mapping to the interaction ID. Required scope: workflow:task:create.

Step 4: Notify Agents via Desktop Notifications

After task creation, the service pushes a desktop notification to the assigned agent using the Notification API.

async function sendDesktopNotification(agentId, taskId, interactionId) {
  const token = await getAccessToken();
  const endpoint = `${BASE_URL}/api/v2/notifications/agents`;

  const notificationPayload = {
    agentId: agentId,
    message: `New Assist Task assigned: ${taskId}. Review interaction ${interactionId}.`,
    type: 'desktop',
    priority: 'high'
  };

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

    console.log('Desktop notification sent:', response.data.id);
    return response.data;
  } catch (error) {
    console.error('Failed to send desktop notification:', error.message);
    // Do not throw here to avoid breaking task creation flow
    return null;
  }
}

The notification payload targets a specific agentId with a desktop type. The call uses the same retry-aware axios instance. Required scope: notification:write.

Complete Working Example

The following script combines all components into a runnable Express application. Replace environment variables with your CXone credentials.

require('dotenv').config();
const express = require('express');
const axios = require('axios');

const CXONE_ORG = process.env.CXONE_ORG;
const CXONE_CLIENT_ID = process.env.CXONE_CLIENT_ID;
const CXONE_CLIENT_SECRET = process.env.CXONE_CLIENT_SECRET;
const BASE_URL = `https://${CXONE_ORG}.api.cxone.com`;

let cachedToken = null;
let tokenExpiry = 0;

async function getAccessToken() {
  if (cachedToken && Date.now() < tokenExpiry) {
    return cachedToken;
  }

  const payload = new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: CXONE_CLIENT_ID,
    client_secret: CXONE_CLIENT_SECRET,
    scope: 'interaction:read postinteractionform:read workflow:task:create notification:write'
  });

  const response = await axios.post(`${BASE_URL}/oauth/token`, payload, {
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
  });

  cachedToken = response.data.access_token;
  tokenExpiry = Date.now() + (response.data.expires_in * 1000) - 30000;
  return cachedToken;
}

const axiosWithRetry = axios.create();
axiosWithRetry.interceptors.response.use(
  response => response,
  async error => {
    const originalConfig = error.config;
    if (!originalConfig) throw error;
    if (error.response?.status === 429 && !originalConfig._retryCount) {
      originalConfig._retryCount = originalConfig._retryCount || 0;
      if (originalConfig._retryCount < 3) {
        originalConfig._retryCount++;
        const retryAfter = error.response.headers['retry-after'] || Math.pow(2, originalConfig._retryCount);
        console.log(`Rate limited. Retrying in ${retryAfter} seconds...`);
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
        return axiosWithRetry(originalConfig);
      }
    }
    return Promise.reject(error);
  }
);

async function fetchPostInteractionForm(interactionId) {
  const token = await getAccessToken();
  const response = await axios.get(`${BASE_URL}/api/v2/interactions/${interactionId}/postinteractionforms`, {
    headers: { Authorization: `Bearer ${token}` },
    validateStatus: status => status === 200 || status === 404
  });
  return response.status === 404 ? null : response.data;
}

function evaluateFormResponses(formData) {
  if (!formData?.responses) return { createTask: false };
  const satisfaction = formData.responses.find(r => r.fieldName === 'overall_satisfaction');
  const score = satisfaction ? parseFloat(satisfaction.value) : 5;
  
  let priority = 'Normal';
  let dueDate = new Date(Date.now() + 7 * 86400000).toISOString();
  
  if (score < 3) { priority = 'High'; dueDate = new Date(Date.now() + 86400000).toISOString(); }
  else if (score < 4) { priority = 'Medium'; dueDate = new Date(Date.now() + 3 * 86400000).toISOString(); }
  
  return { createTask: score < 4, priority, dueDate, satisfactionScore: score };
}

async function createWorkflowTask(interactionId, agentId, priority, dueDate, satisfactionScore) {
  const token = await getAccessToken();
  const response = await axiosWithRetry.post(`${BASE_URL}/api/v2/workflows/tasks`, {
    subject: `Agent Assist: Follow-up required for interaction ${interactionId}`,
    description: `Post-interaction satisfaction score: ${satisfactionScore}. Requires agent review.`,
    dueDate,
    priority,
    assigneeId: agentId,
    customFields: [{ id: 'interaction_id_link', value: interactionId }]
  }, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } });
  return response.data;
}

async function sendDesktopNotification(agentId, taskId, interactionId) {
  const token = await getAccessToken();
  try {
    await axiosWithRetry.post(`${BASE_URL}/api/v2/notifications/agents`, {
      agentId,
      message: `New Assist Task assigned: ${taskId}. Review interaction ${interactionId}.`,
      type: 'desktop',
      priority: 'high'
    }, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } });
  } catch (err) {
    console.error('Notification failed:', err.message);
  }
}

async function processInteractionCompletion(interactionId, agentId) {
  try {
    const formData = await fetchPostInteractionForm(interactionId);
    const evaluation = evaluateFormResponses(formData);
    
    if (!evaluation.createTask) {
      console.log('No task required for this interaction');
      return;
    }

    const task = await createWorkflowTask(interactionId, agentId, evaluation.priority, evaluation.dueDate, evaluation.satisfactionScore);
    await sendDesktopNotification(agentId, task.id, interactionId);
  } catch (error) {
    console.error('Processing failed:', error.response?.data || error.message);
  }
}

const app = express();
app.use(express.json());

app.post('/webhooks/interaction-completed', (req, res) => {
  const { interactionId, agentId } = req.body;
  if (!interactionId || !agentId) return res.status(400).json({ error: 'Missing interactionId or agentId' });
  res.status(200).json({ received: true });
  processInteractionCompletion(interactionId, agentId).catch(console.error);
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Service listening on port ${PORT}`));

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: Expired access token, invalid client credentials, or missing Authorization header.
  • Fix: Verify CXONE_CLIENT_ID and CXONE_CLIENT_SECRET in the environment. Ensure the token cache expiration logic subtracts a buffer. Check that the getAccessToken function runs before every API call.
  • Code: The provided implementation handles automatic refresh. If 401 persists, log cachedToken and tokenExpiry to confirm cache invalidation.

Error: 403 Forbidden

  • Cause: OAuth client lacks required scopes, or the agent/task IDs reference resources outside the client permissions.
  • Fix: Regenerate the OAuth client with interaction:read, postinteractionform:read, workflow:task:create, and notification:write. Verify the custom field ID (interaction_id_link) matches your CXone environment configuration.
  • Code: Add scope validation during initialization: if (!process.env.CXONE_CLIENT_ID) throw new Error('Missing OAuth credentials');

Error: 429 Too Many Requests

  • Cause: Exceeding CXone API rate limits during bulk event processing or rapid task creation.
  • Fix: The axiosWithRetry interceptor implements exponential backoff. If cascading 429s occur, implement a request queue with concurrency limiting.
  • Code: The interceptor checks error.response.headers['retry-after'] and delays execution. Monitor Retry-After values to tune queue throughput.

Error: 500 Internal Server Error

  • Cause: Invalid task payload structure, malformed dueDate, or missing required workflow fields.
  • Fix: Validate dueDate as ISO 8601 string. Ensure priority matches CXone enum values (Low, Normal, Medium, High, Critical). Verify assigneeId references an active user.
  • Code: Add payload validation before POST: if (!taskPayload.dueDate || isNaN(Date.parse(taskPayload.dueDate))) throw new Error('Invalid dueDate format');

Official References