Invoking Cognigy External Actions via REST API with Node.js
What You Will Build
- A Node.js service that constructs and dispatches invocation payloads to Cognigy.AI external actions with structured input parameters, context session data, and enforced timeout constraints.
- This implementation uses the Cognigy.AI v3 External Action API and native Node.js HTTP clients.
- The code covers JavaScript with async webhook callback handling, Zod schema validation, fallback injection, structured audit logging, and metrics emission for monitoring dashboards.
Prerequisites
- OAuth 2.0 Client Credentials grant type with
cognigy:external-actions:executeandcognigy:api:readscopes. - Cognigy.AI API v3 base URL (
https://{tenant}.cognigy.ai/api/v3/). - Node.js 18+ runtime with ES Module support.
- External dependencies:
axios,zod,pino,uuid. Install vianpm install axios zod pino uuid.
Authentication Setup
Cognigy.AI requires a bearer token for all v3 API calls. The client credentials flow exchanges a client ID and secret for a short-lived token. Production implementations must cache tokens and refresh before expiration to avoid authentication latency.
import axios from 'axios';
const COGNIGY_TENANT = process.env.COGNIGY_TENANT || 'demo';
const COGNIGY_API_BASE = `https://${COGNIGY_TENANT}.cognigy.ai/api/v3`;
const OAUTH_TOKEN_URL = `https://${COGNIGY_TENANT}.cognigy.ai/oauth/token`;
let cachedToken = null;
let tokenExpiry = 0;
/**
* Retrieves or caches an OAuth2 bearer token.
* Required Scope: cognigy:external-actions:execute cognigy:api:read
*/
export async function getAccessToken() {
const now = Date.now();
if (cachedToken && now < tokenExpiry) {
return cachedToken;
}
try {
const response = await axios.post(OAUTH_TOKEN_URL, new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.COGNIGY_CLIENT_ID,
client_secret: process.env.COGNIGY_CLIENT_SECRET,
scope: 'cognigy:external-actions:execute cognigy:api:read'
}), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
cachedToken = response.data.access_token;
// Subtract 5 seconds to prevent edge-case expiration during network transit
tokenExpiry = now + (response.data.expires_in * 1000) - 5000;
return cachedToken;
} catch (error) {
const errorMessage = error.response?.data?.error_description || error.message;
throw new Error(`OAuth token retrieval failed: ${errorMessage}`);
}
}
Implementation
Step 1: Construct Invocation Payloads and Validate Constraints
External actions require strict payload structure and timeout boundaries to prevent session hangs. This step defines validation schemas, enforces timeout thresholds, and verifies external service availability before dispatching.
import { z } from 'zod';
import { randomUUID } from 'crypto';
const ActionInputSchema = z.object({
userId: z.string().uuid(),
amount: z.number().positive(),
currency: z.string().length(3)
});
const ContextSchema = z.object({
sessionId: z.string().min(1),
locale: z.string().default('en-US'),
channel: z.enum(['web', 'mobile', 'voice']).default('web')
});
const MAX_TIMEOUT_MS = 30000;
/**
* Validates input parameters, context data, and timeout constraints.
*/
export function validateConstraints(inputParams, contextData, timeoutMs) {
const validatedInput = ActionInputSchema.parse(inputParams);
const validatedContext = ContextSchema.parse(contextData);
if (timeoutMs > MAX_TIMEOUT_MS) {
throw new Error(`Timeout threshold exceeded. Maximum allowed is ${MAX_TIMEOUT_MS}ms.`);
}
return {
validatedInput,
validatedContext,
effectiveTimeout: timeoutMs || 10000
};
}
/**
* Checks external service availability before invocation.
* Replace the health check URL with your actual dependency endpoint.
*/
export async function checkExternalAvailability(healthUrl) {
try {
await axios.get(healthUrl, { timeout: 2000 });
return true;
} catch {
return false;
}
}
Step 2: Invoke Action and Handle Asynchronous Execution
The Cognigy External Action API supports both synchronous and asynchronous execution modes. When a callbackUrl is provided, the API returns 202 Accepted and dispatches the result to your webhook endpoint upon completion. This step implements the HTTP call with retry logic for 429 rate limits.
/**
* Retries requests on 429 rate limit with exponential backoff.
*/
async function executeWithRetry(requestFn, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await requestFn();
} catch (error) {
if (error.response?.status === 429 && attempt < maxRetries) {
const backoff = Math.pow(2, attempt) * 1000;
console.warn(`Rate limited (429). Retrying in ${backoff}ms...`);
await new Promise(resolve => setTimeout(resolve, backoff));
} else {
throw error;
}
}
}
}
/**
* Dispatches the external action invocation to Cognigy.AI.
* Required Scope: cognigy:external-actions:execute
*/
export async function invokeAction(actionId, input, context, timeoutMs, callbackUrl) {
const token = await getAccessToken();
const { validatedInput, validatedContext, effectiveTimeout } = validateConstraints(input, context, timeoutMs);
const payload = {
actionId,
input: validatedInput,
context: validatedContext,
timeout: effectiveTimeout,
async: !!callbackUrl,
callbackUrl: callbackUrl || undefined
};
const requestFn = () => axios.post(`${COGNIGY_API_BASE}/external-actions/invoke`, payload, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
'X-Request-Id': randomUUID()
},
timeout: effectiveTimeout + 5000,
maxRedirects: 0
});
const response = await executeWithRetry(requestFn);
return {
statusCode: response.status,
data: response.data,
headers: response.headers
};
}
Expected HTTP Request:
POST /api/v3/external-actions/invoke HTTP/1.1
Host: demo.cognigy.ai
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
X-Request-Id: f47ac10b-58cc-4372-a567-0e02b2c3d479
{
"actionId": "ext-payment-processor-v2",
"input": {
"userId": "550e8400-e29b-41d4-a716-446655440000",
"amount": 150.00,
"currency": "USD"
},
"context": {
"sessionId": "sess_8a7b6c5d4e3f",
"locale": "en-US",
"channel": "web"
},
"timeout": 15000,
"async": true,
"callbackUrl": "https://my-app.example.com/webhooks/cognigy-callback"
}
Expected HTTP Response (Async):
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"requestId": "req_9f8e7d6c5b4a",
"status": "queued",
"estimatedCompletionMs": 12000
}
Step 3: Response Parsing, Schema Validation, and Fallback Injection
External services may return malformed payloads or unexpected structures. This step validates the response against a strict schema and injects fallback values when validation fails, ensuring the bot conversation never breaks.
const ActionOutputSchema = z.object({
status: z.enum(['success', 'partial', 'failed']),
transactionId: z.string().optional(),
result: z.any()
});
/**
* Parses raw action response, validates against schema, and injects fallback data on failure.
*/
export function parseActionResponse(rawResponse, fallbackData) {
try {
const parsed = ActionOutputSchema.parse(rawResponse.data || rawResponse);
return { success: true, data: parsed };
} catch (validationError) {
console.warn('Schema validation failed for Cognigy action response. Injecting fallback values.');
return {
success: false,
data: {
status: 'failed',
transactionId: null,
result: fallbackData,
validationErrors: validationError.errors
}
};
}
}
Step 4: Metrics Synchronization, Audit Logging, and Webhook Handling
Production bots require observability. This step calculates invocation latency, tracks error frequencies, emits structured audit logs, and provides a webhook handler that transforms payloads for seamless bot integration.
import pino from 'pino';
const logger = pino({ level: 'info', transport: { target: 'pino-pretty' } });
class ActionMetricsTracker {
constructor(eventStreamUrl) {
this.eventStreamUrl = eventStreamUrl || 'https://monitoring.example.com/ingest';
this.invocationCount = 0;
this.errorCount = 0;
this.totalLatency = 0;
}
recordInvocation(actionId, sessionId, latencyMs, isError) {
this.invocationCount++;
if (isError) this.errorCount++;
this.totalLatency += latencyMs;
const auditLog = {
timestamp: new Date().toISOString(),
actionId,
sessionId,
latencyMs,
status: isError ? 'ERROR' : 'SUCCESS',
source: 'cognigy-invoker-node'
};
logger.info(auditLog, 'Action Execution Audit Log');
this.emitToEventStream(auditLog);
}
async emitToEventStream(event) {
try {
await axios.post(this.eventStreamUrl, event, {
headers: { 'Content-Type': 'application/json' },
timeout: 3000
});
} catch (error) {
logger.error({ error: error.message }, 'Failed to emit metrics to event stream');
}
}
getMetrics() {
return {
totalInvocations: this.invocationCount,
totalErrors: this.errorCount,
averageLatencyMs: this.invocationCount > 0 ? Math.round(this.totalLatency / this.invocationCount) : 0,
errorRate: this.invocationCount > 0 ? parseFloat((this.errorCount / this.invocationCount).toFixed(4)) : 0
};
}
}
/**
* Express/Fastify compatible webhook handler for async Cognigy callbacks.
*/
export function createWebhookHandler(metricsTracker) {
return async function handleWebhookCallback(req, res) {
const startTime = Date.now();
const { actionId, sessionId, payload } = req.body;
const parsedResult = parseActionResponse(payload, { status: 'fallback', message: 'External service returned invalid format' });
const latencyMs = Date.now() - startTime;
metricsTracker.recordInvocation(actionId, sessionId, latencyMs, !parsedResult.success);
const botResponse = {
text: parsedResult.success ? 'External action completed successfully.' : 'External action encountered an issue.',
data: parsedResult.data.result,
metadata: { latencyMs, actionId, status: parsedResult.data.status }
};
res.status(200).json(botResponse);
};
}
Complete Working Example
The following module combines authentication, validation, invocation, parsing, metrics, and webhook handling into a single production-ready invoker class. Execute with node cognigy-invoker.js.
import axios from 'axios';
import { z } from 'zod';
import pino from 'pino';
import { randomUUID } from 'crypto';
import http from 'http';
// Configuration
const COGNIGY_TENANT = process.env.COGNIGY_TENANT || 'demo';
const COGNIGY_API_BASE = `https://${COGNIGY_TENANT}.cognigy.ai/api/v3`;
const OAUTH_TOKEN_URL = `https://${COGNIGY_TENANT}.cognigy.ai/oauth/token`;
const MAX_TIMEOUT_MS = 30000;
const FALLBACK_DATA = { status: 'fallback', message: 'Service unavailable, using default response' };
// Schemas
const ActionInputSchema = z.object({ userId: z.string().uuid(), amount: z.number().positive(), currency: z.string().length(3) });
const ContextSchema = z.object({ sessionId: z.string().min(1), locale: z.string().default('en-US'), channel: z.enum(['web', 'mobile', 'voice']).default('web') });
const ActionOutputSchema = z.object({ status: z.enum(['success', 'partial', 'failed']), transactionId: z.string().optional(), result: z.any() });
// State
let cachedToken = null;
let tokenExpiry = 0;
const metricsTracker = new (class {
constructor() { this.invocationCount = 0; this.errorCount = 0; this.totalLatency = 0; }
recordInvocation(actionId, sessionId, latencyMs, isError) {
this.invocationCount++;
if (isError) this.errorCount++;
this.totalLatency += latencyMs;
console.log(`[AUDIT] action=${actionId} session=${sessionId} latency=${latencyMs}ms status=${isError ? 'ERROR' : 'SUCCESS'}`);
console.log(`[METRICS] invocations=${this.invocationCount} errors=${this.errorCount} avgLatency=${Math.round(this.totalLatency/this.invocationCount)}ms errorRate=${(this.errorCount/this.invocationCount).toFixed(2)}`);
}
})();
async function getAccessToken() {
const now = Date.now();
if (cachedToken && now < tokenExpiry) return cachedToken;
try {
const res = await axios.post(OAUTH_TOKEN_URL, new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.COGNIGY_CLIENT_ID,
client_secret: process.env.COGNIGY_CLIENT_SECRET,
scope: 'cognigy:external-actions:execute cognigy:api:read'
}), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
cachedToken = res.data.access_token;
tokenExpiry = now + (res.data.expires_in * 1000) - 5000;
return cachedToken;
} catch (err) {
throw new Error(`OAuth failed: ${err.response?.data?.error_description || err.message}`);
}
}
async function executeWithRetry(fn, retries = 3) {
for (let i = 1; i <= retries; i++) {
try { return await fn(); }
catch (err) {
if (err.response?.status === 429 && i < retries) {
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
} else { throw err; }
}
}
}
export async function invokeAction(actionId, input, context, timeoutMs, callbackUrl) {
const token = await getAccessToken();
const validatedInput = ActionInputSchema.parse(input);
const validatedContext = ContextSchema.parse(context);
if (timeoutMs > MAX_TIMEOUT_MS) throw new Error(`Timeout exceeds maximum ${MAX_TIMEOUT_MS}ms`);
const payload = { actionId, input: validatedInput, context: validatedContext, timeout: timeoutMs || 10000, async: !!callbackUrl, callbackUrl: callbackUrl || undefined };
const startTime = Date.now();
const response = await executeWithRetry(() => axios.post(`${COGNIGY_API_BASE}/external-actions/invoke`, payload, {
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', 'X-Request-Id': randomUUID() },
timeout: (timeoutMs || 10000) + 5000
}));
const latencyMs = Date.now() - startTime;
const isError = response.status >= 400;
metricsTracker.recordInvocation(actionId, validatedContext.sessionId, latencyMs, isError);
return response;
}
function parseActionResponse(rawData) {
try { return { success: true, data: ActionOutputSchema.parse(rawData) }; }
catch { return { success: false, data: { status: 'failed', transactionId: null, result: FALLBACK_DATA } }; }
}
function startWebhookServer(port) {
const server = http.createServer(async (req, res) => {
if (req.url !== '/webhooks/cognigy-callback' || req.method !== 'POST') {
res.writeHead(404); return res.end();
}
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
const parsed = parseActionResponse(JSON.parse(body));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ text: parsed.success ? 'Completed' : 'Failed', data: parsed.data.result }));
});
});
server.listen(port, () => console.log(`Webhook listener active on port ${port}`));
return server;
}
// Execution Demo
async function main() {
console.log('Starting Cognigy Action Invoker Demo...');
startWebhookServer(3000);
try {
const result = await invokeAction(
'ext-payment-processor-v2',
{ userId: '550e8400-e29b-41d4-a716-446655440000', amount: 150.00, currency: 'USD' },
{ sessionId: 'sess_8a7b6c5d4e3f', locale: 'en-US', channel: 'web' },
15000,
'https://my-app.example.com/webhooks/cognigy-callback'
);
console.log('Invocation dispatched:', result.data);
} catch (error) {
console.error('Invocation failed:', error.message);
}
}
main().catch(console.error);
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token has expired, the client credentials are incorrect, or the token cache is not refreshing.
- Fix: Verify
COGNIGY_CLIENT_IDandCOGNIGY_CLIENT_SECRETenvironment variables. Ensure the token cache expiration logic subtracts a buffer period. Clear the cache manually during development to force a fresh token fetch.
Error: 403 Forbidden
- Cause: The OAuth token lacks the
cognigy:external-actions:executescope, or the target action is not published in the Cognigy.AI console. - Fix: Regenerate the token with the correct scope string. Verify the action status in the Cognigy.AI dashboard. External actions must be in
Publishedstate to accept API invocations.
Error: 408 Request Timeout or 504 Gateway Timeout
- Cause: The external service exceeded the
timeoutthreshold defined in the payload, or the client HTTP timeout is too low. - Fix: Increase the
timeoutparameter in the invocation payload. Ensure the client-sideaxios.timeoutis set topayload.timeout + 5000to allow network overhead. Implement circuit breakers if the downstream service is consistently unresponsive.
Error: 422 Unprocessable Entity
- Cause: The request payload violates Cognigy schema requirements, or the
actionIdreferences a non-existent action. - Fix: Validate the payload structure against the Zod schemas before dispatch. Ensure
actionIdmatches the exact identifier from the Cognigy.AI platform. Check thatinputandcontextobjects contain only defined fields.