Developing Custom NICE Cognigy.AI Action Nodes with Node.js
What You Will Build
- A production-ready custom action node that retrieves inventory data from an external microservice, applies circuit breaker protection, and returns structured output to the Cognigy.AI conversational flow.
- This implementation uses the Cognigy.AI Action interface, Node.js ES Modules, and the Cognigy.AI REST API for repository publishing.
- All code is written in modern JavaScript with explicit async/await patterns, structured logging, and comprehensive error handling.
Prerequisites
- Cognigy.AI organization with programmatic API access and a valid client ID/secret or API key
- Node.js 18+ runtime environment
- Required npm packages:
@cognigy/sdk,axios,opossum,winston,nock,jest,dotenv - Required OAuth scopes:
actions:read,actions:write,actions:publish,repository:write - Access to an external inventory microservice endpoint (or a local mock server)
Authentication Setup
Cognigy.AI uses Bearer token authentication for all REST API interactions. The token must be generated via the Cognigy.AI authentication endpoint and attached to every request. Token expiration is typically thirty minutes, so production implementations must implement refresh logic or short-lived token caching.
The following code demonstrates token acquisition and validation. It uses axios with interceptors to automatically attach the Bearer token and handle 401 Unauthorized responses by attempting a token refresh.
// auth.js
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
const COGNIGY_BASE_URL = process.env.COGNIGY_BASE_URL || 'https://your-org.cognigy.ai';
const CLIENT_ID = process.env.COGNIGY_CLIENT_ID;
const CLIENT_SECRET = process.env.COGNIGY_CLIENT_SECRET;
let accessToken = null;
let tokenExpiry = 0;
export const getToken = async () => {
if (accessToken && Date.now() < tokenExpiry) {
return accessToken;
}
try {
const response = await axios.post(`${COGNIGY_BASE_URL}/api/v1/auth/token`, {
grant_type: 'client_credentials',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
scope: 'actions:read actions:write actions:publish repository:write'
}, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
accessToken = response.data.access_token;
tokenExpiry = Date.now() + (response.data.expires_in * 1000) - 5000; // Refresh 5s early
return accessToken;
} catch (error) {
if (error.response && error.response.status === 401) {
throw new Error('Authentication failed. Verify CLIENT_ID and CLIENT_SECRET.');
}
throw new Error(`Token acquisition failed: ${error.message}`);
}
};
export const createCognigyApiClient = () => {
return axios.create({
baseURL: COGNIGY_BASE_URL,
headers: { 'Content-Type': 'application/json' }
});
};
// Attach token interceptor
const apiClient = createCognigyApiClient();
apiClient.interceptors.request.use(async (config) => {
const token = await getToken();
config.headers.Authorization = `Bearer ${token}`;
return config;
});
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response && error.response.status === 401) {
accessToken = null;
tokenExpiry = 0;
const newToken = await getToken();
error.config.headers.Authorization = `Bearer ${newToken}`;
return axios(error.config);
}
return Promise.reject(error);
}
);
export { apiClient };
Implementation
Step 1: Define Action Schema with Input/Output Bindings
Cognigy.AI actions require a schema definition that declares expected inputs and guaranteed outputs. The schema drives the UI in the Cognigy.AI flow editor and enforces type safety at runtime. Each binding must specify a data type, a description, and whether it is required.
// actionSchema.js
export const actionSchema = {
name: 'checkInventoryStatus',
description: 'Queries external inventory microservice and returns stock availability',
inputs: {
productId: {
type: 'string',
required: true,
description: 'Unique identifier for the product'
},
warehouseId: {
type: 'string',
required: false,
description: 'Optional warehouse location filter'
}
},
outputs: {
stockLevel: {
type: 'number',
description: 'Current available quantity'
},
availability: {
type: 'boolean',
description: 'Whether the product is in stock'
},
errorMessage: {
type: 'string',
description: 'Error details if the request fails'
}
}
};
The schema object is exported and attached to the action module. Cognigy.AI validates incoming flow variables against this structure before invoking the execute method. Missing required inputs cause immediate flow termination with a validation error.
Step 2: Implement Business Logic to Interact with External Microservices
The core action logic resides in the execute function. This function receives the input payload, calls the external microservice, transforms the response, and returns the output payload. The external service uses a standard REST pattern.
// externalService.js
import axios from 'axios';
const INVENTORY_SERVICE_URL = process.env.INVENTORY_SERVICE_URL || 'https://inventory-api.example.com';
export const fetchInventory = async (productId, warehouseId) => {
const params = new URLSearchParams({ productId });
if (warehouseId) {
params.append('warehouseId', warehouseId);
}
const response = await axios.get(`${INVENTORY_SERVICE_URL}/api/v2/stock`, {
params,
headers: {
'Accept': 'application/json',
'X-Request-Id': crypto.randomUUID()
},
timeout: 5000
});
return response.data;
};
The request includes a X-Request-Id header for distributed tracing. The timeout parameter is set at five seconds to prevent indefinite hanging. The response body follows this structure:
{
"productId": "PROD-8842",
"warehouseId": "WH-EU-01",
"stockLevel": 142,
"lastUpdated": "2024-01-15T09:30:00Z",
"status": "available"
}
Error handling must catch network failures and HTTP errors. The axios instance will throw on status codes outside the 2xx range. You must map these to the action output structure.
Step 3: Manage Asynchronous Operations, Timeouts, and Circuit Breakers
External microservices experience latency spikes and outages. A circuit breaker pattern prevents cascading failures by failing fast when the downstream service is unhealthy. The opossum library implements the Hystrix-style circuit breaker for Node.js.
// circuitBreaker.js
import Hystrix from 'opossum';
import { fetchInventory } from './externalService.js';
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()]
});
const options = {
errorThresholdPercentage: 50,
requestVolumeThreshold: 10,
sleepWindowMs: 30000,
timeout: 5000,
checkInvocations: false,
name: 'inventoryCircuitBreaker'
};
const inventoryBreaker = Hystrix(fetchInventory, options);
inventoryBreaker.on('open', () => {
logger.warn({ event: 'circuit_breaker_open', service: 'inventory' });
});
inventoryBreaker.on('close', () => {
logger.info({ event: 'circuit_breaker_close', service: 'inventory' });
});
inventoryBreaker.on('timeout', () => {
logger.error({ event: 'circuit_breaker_timeout', service: 'inventory' });
});
export const executeWithCircuitBreaker = async (productId, warehouseId) => {
try {
const data = await inventoryBreaker(productId, warehouseId);
return {
stockLevel: data.stockLevel || 0,
availability: data.status === 'available',
errorMessage: null
};
} catch (error) {
logger.error({
event: 'action_execution_error',
productId,
error: error.message,
circuitState: inventoryBreaker.isCircuitOpen() ? 'open' : 'closed'
});
return {
stockLevel: 0,
availability: false,
errorMessage: `Inventory service unavailable: ${error.message}`
};
}
};
The circuit breaker opens after fifty percent of requests fail within a rolling window. Once open, subsequent requests fail immediately without hitting the external service. The sleepWindowMs defines how long the breaker remains open before attempting a half-open test request. Timeout handling is built into the opossum configuration. Promise chaining is replaced by explicit async/await for readability and stack trace preservation.
Step 4: Log Action Execution Traces and Test with Mock Service Responses
Production actions require structured logging for debugging. The winston logger captures execution context, timing, and circuit breaker state. Testing requires mocking the external HTTP call to avoid network dependencies during CI/CD pipelines.
// test/inventoryAction.test.js
import nock from 'nock';
import { fetchInventory } from '../externalService.js';
describe('fetchInventory', () => {
afterEach(() => {
nock.cleanAll();
});
test('returns stock data for valid product', async () => {
nock('https://inventory-api.example.com')
.get('/api/v2/stock')
.query({ productId: 'PROD-8842' })
.reply(200, {
productId: 'PROD-8842',
warehouseId: 'WH-EU-01',
stockLevel: 50,
lastUpdated: '2024-01-15T09:30:00Z',
status: 'available'
}, {
'X-Request-Id': 'test-123'
});
const result = await fetchInventory('PROD-8842', 'WH-EU-01');
expect(result.stockLevel).toBe(50);
expect(result.status).toBe('available');
});
test('throws error on service failure', async () => {
nock('https://inventory-api.example.com')
.get('/api/v2/stock')
.query({ productId: 'PROD-9999' })
.reply(500, { error: 'Internal Server Error' });
await expect(fetchInventory('PROD-9999')).rejects.toThrow();
});
});
The nock library intercepts HTTP requests at the Node.js level. It matches the method, path, query parameters, and returns a predefined response. This ensures deterministic test execution. The test suite validates both success and failure paths without requiring the actual microservice.
Step 5: Publish Action Definitions to the Cognigy Repository
After validation and testing, the action definition must be registered in the Cognigy.AI repository. The platform provides a REST endpoint for programmatic publishing. The payload includes the schema, execution entry point, and metadata.
// publishAction.js
import { apiClient } from './auth.js';
import { actionSchema } from './actionSchema.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const publishAction = async () => {
const actionCode = fs.readFileSync(path.join(__dirname, 'action.js'), 'utf8');
const payload = {
name: actionSchema.name,
description: actionSchema.description,
schema: actionSchema,
code: actionCode,
version: '1.0.0',
category: 'inventory',
tags: ['microservice', 'circuit-breaker', 'nodejs']
};
try {
const response = await apiClient.post('/api/v1/actions', payload);
console.log('Action published successfully:', response.data.id);
return response.data;
} catch (error) {
if (error.response && error.response.status === 409) {
console.warn('Action already exists. Updating existing definition...');
const updateResponse = await apiClient.put(`/api/v1/actions/${payload.name}`, payload);
return updateResponse.data;
}
if (error.response && error.response.status === 429) {
const retryAfter = error.response.headers['retry-after'] || 2;
console.warn(`Rate limited. Retrying in ${retryAfter} seconds...`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
return publishAction();
}
throw new Error(`Publish failed: ${error.message}`);
}
};
The publishing script reads the compiled action code, attaches the schema, and sends a POST request to /api/v1/actions. The required OAuth scope is actions:publish and repository:write. The code handles 409 Conflict by falling back to a PUT update request. It also implements retry logic for 429 Too Many Requests responses by reading the Retry-After header. Pagination is not applicable to single resource creation, but rate limit handling ensures reliable deployment in CI/CD pipelines.
Complete Working Example
The following module combines schema definition, circuit breaker execution, logging, and Cognigy.AI action interface compliance into a single deployable file.
// action.js
import { actionSchema } from './actionSchema.js';
import { executeWithCircuitBreaker } from './circuitBreaker.js';
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [new winston.transports.Console()]
});
export const execute = async (input) => {
const startTime = Date.now();
logger.info({
event: 'action_start',
action: 'checkInventoryStatus',
input
});
try {
const result = await executeWithCircuitBreaker(input.productId, input.warehouseId);
const executionTime = Date.now() - startTime;
logger.info({
event: 'action_complete',
action: 'checkInventoryStatus',
executionTimeMs: executionTime,
output: result
});
return {
outputs: result,
next: 'default'
};
} catch (error) {
const executionTime = Date.now() - startTime;
logger.error({
event: 'action_failure',
action: 'checkInventoryStatus',
executionTimeMs: executionTime,
error: error.message
});
return {
outputs: {
stockLevel: 0,
availability: false,
errorMessage: error.message
},
next: 'errorHandler'
};
}
};
export { actionSchema };
The execute function matches the Cognigy.AI action interface. It accepts an input object containing the bound variables, processes them through the protected microservice call, and returns an object with outputs and next routing instructions. The next field determines which node executes after completion. Structured logging captures timing and payload context for observability platforms.
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: Expired Bearer token, missing
Authorizationheader, or insufficient OAuth scopes. - Fix: Verify the token generation endpoint returns a valid JWT. Ensure the
scopeparameter includesactions:read actions:write actions:publish repository:write. Implement token refresh logic before expiration. - Code Fix: The
auth.jsinterceptor automatically detects401responses, clears the cached token, fetches a new one, and retries the original request.
Error: 429 Too Many Requests
- Cause: Exceeding Cognigy.AI rate limits or external microservice throttling.
- Fix: Implement exponential backoff and respect the
Retry-Afterheader. Batch requests where possible. - Code Fix: The
publishActionfunction readsRetry-Afterand delays execution. For the inventory service, the circuit breaker naturally reduces request volume during outages.
Error: Circuit Breaker Open
- Cause: The external inventory service failed repeatedly, triggering the failure threshold.
- Fix: Wait for the
sleepWindowMsperiod to expire. The breaker will transition to half-open and allow a test request. If successful, it closes and resumes normal traffic. - Code Fix: The
opossumconfiguration handles state transitions automatically. TheexecuteWithCircuitBreakerfunction returns a fallback payload when the breaker is open, preventing flow crashes.
Error: Timeout Exceeded
- Cause: External service response time exceeds the configured
timeoutvalue. - Fix: Increase the timeout if the service legitimately requires more time, or optimize the downstream query. Implement request cancellation via
AbortControllerfor long-running flows. - Code Fix: The
axiostimeout is set to five seconds. The circuit breaker timeout matches this value. Both trigger fast failures and log timeout events for monitoring.