Polling Cognigy.AI Model Training Jobs with TypeScript
What You Will Build
- A TypeScript module that polls Cognigy.AI NLU training jobs using atomic GET requests, validates polling intervals, enforces abort directives, tracks latency, generates audit logs, and exposes callbacks for external orchestration.
- The implementation uses the Cognigy.AI REST API v2 surface for training job lifecycle management.
- The code is written in TypeScript and runs on Node.js 18 or later.
Prerequisites
- OAuth2 client credentials with the
cognigy.ai:api:readscope - Cognigy.AI API v2
- Node.js 18+ with native
fetchsupport - Dependencies:
typescript,zod,dotenv,@types/node - Project initialized with
npm init -yand compiled withts-nodeortsc
Authentication Setup
Cognigy.AI requires a bearer token for all training API calls. The client credentials flow exchanges your client ID and secret for a short-lived token. The following code demonstrates token acquisition, in-memory caching, and automatic refresh when the token expires.
import dotenv from 'dotenv';
dotenv.config();
export interface CognigyTokenResponse {
access_token: string;
token_type: string;
expires_in: number;
scope: string;
}
const COGNIGY_BASE_URL = process.env.COGNIGY_HOST || 'https://your-instance.cognigy.ai';
const AUTH_ENDPOINT = `${COGNIGY_BASE_URL}/api/v2/auth/token`;
export class CognigyAuthClient {
private token: string | null = null;
private expiry: number = 0;
async getToken(): Promise<string> {
if (this.token && Date.now() < this.expiry) {
return this.token;
}
const params = new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.COGNIGY_CLIENT_ID || '',
client_secret: process.env.COGNIGY_CLIENT_SECRET || '',
scope: 'cognigy.ai:api:read'
});
const response = await fetch(AUTH_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Token acquisition failed (${response.status}): ${errorBody}`);
}
const data: CognigyTokenResponse = await response.json();
this.token = data.access_token;
this.expiry = Date.now() + (data.expires_in * 1000) - 5000; // 5s buffer
return this.token;
}
}
Implementation
Step 1: Poll Configuration Schema & Validation
The polling engine requires a structured configuration containing the job UUID, checkpoint interval matrix, and abort directives. We validate this configuration against Cognigy.AI training service constraints to prevent polling failure and enforce maximum frequency limits.
import { z } from 'zod';
const MIN_INTERVAL_MS = 5000; // Cognigy.AI enforces minimum 5s between status checks
const MAX_CHECKPOINTS = 10;
export const PollConfigSchema = z.object({
jobId: z.string().uuid('jobId must be a valid UUID'),
checkpointIntervals: z.array(z.number().int().positive())
.min(1, 'At least one checkpoint interval is required')
.max(MAX_CHECKPOINTS, `Maximum ${MAX_CHECKPOINTS} checkpoints allowed`)
.refine(arr => arr.every(interval => interval >= MIN_INTERVAL_MS), {
message: `Each interval must be at least ${MIN_INTERVAL_MS}ms to respect API rate limits`
}),
abortOnThreshold: z.number().int().positive(),
maxRetries: z.number().int().nonnegative().default(3)
});
export type PollConfig = z.infer<typeof PollConfigSchema>;
export function validatePollConfig(config: unknown): PollConfig {
const result = PollConfigSchema.safeParse(config);
if (!result.success) {
const errors = result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join('; ');
throw new Error(`Poll configuration validation failed: ${errors}`);
}
return result.data;
}
Step 2: Atomic GET Status Check & Abort Logic
Status checks must be atomic to avoid race conditions during Cognigy.AI scaling events. We use an AbortController to enforce the abort directive and verify the response payload format before processing.
interface TrainingStatusResponse {
status: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED';
progress: number;
checkpoint?: number;
error?: string;
}
export async function fetchTrainingStatus(
jobId: string,
token: string,
signal: AbortSignal
): Promise<TrainingStatusResponse> {
const url = `${COGNIGY_BASE_URL}/api/v2/nlu/training/${jobId}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
signal
});
if (response.status === 401) throw new Error('UNAUTHORIZED: Token expired or invalid');
if (response.status === 403) throw new Error('FORBIDDEN: Insufficient OAuth scopes');
if (response.status === 429) throw new Error('RATE_LIMITED: 429 Too Many Requests');
if (response.status >= 500) throw new Error(`SERVER_ERROR: ${response.status}`);
if (!response.ok) {
const errText = await response.text();
throw new Error(`STATUS_CHECK_FAILED: ${response.status} - ${errText}`);
}
const data: unknown = await response.json();
// Format verification
if (!data || typeof data !== 'object' || !('status' in data)) {
throw new Error('PAYLOAD_MISMATCH: Response does not match TrainingStatusResponse schema');
}
return data as TrainingStatusResponse;
}
Step 3: Quota Checking, Error Classification & Latency Tracking
We implement a sliding window quota checker to prevent orphaned jobs during scaling. Error codes are classified into retryable, fatal, or abort categories. Latency and checkpoint success rates are tracked for poll efficiency metrics.
export interface PollMetrics {
totalLatencyMs: number;
checkpointSuccessCount: number;
checkpointTotalCount: number;
requestsInWindow: number;
windowStart: number;
}
export class PollValidator {
private metrics: PollMetrics;
private readonly windowMs = 60000; // 60s window for rate limiting
private readonly maxRequestsPerWindow = 12; // Cognigy.AI safe limit
constructor() {
this.metrics = {
totalLatencyMs: 0,
checkpointSuccessCount: 0,
checkpointTotalCount: 0,
requestsInWindow: 0,
windowStart: Date.now()
};
}
checkQuota(): boolean {
const now = Date.now();
if (now - this.metrics.windowStart > this.windowMs) {
this.metrics.requestsInWindow = 0;
this.metrics.windowStart = now;
}
this.metrics.requestsInWindow++;
return this.metrics.requestsInWindow <= this.maxRequestsPerWindow;
}
classifyError(status: number): 'RETRY' | 'ABORT' | 'FATAL' {
if (status === 429 || status === 502 || status === 503 || status === 504) return 'RETRY';
if (status === 400 || status === 404) return 'ABORT';
if (status === 401 || status === 403) return 'FATAL';
return 'RETRY';
}
recordLatency(ms: number) {
this.metrics.totalLatencyMs += ms;
}
recordCheckpoint(success: boolean) {
this.metrics.checkpointTotalCount++;
if (success) this.metrics.checkpointSuccessCount++;
}
getMetrics(): PollMetrics {
return { ...this.metrics };
}
}
Step 4: Callback Synchronization & Audit Logging
External ML orchestration tools require synchronized status callbacks. We expose a callback interface and generate structured audit logs for AI governance compliance.
export type StatusCallback = (jobId: string, status: string, progress: number, checkpoint?: number) => void;
export type AuditLogCallback = (entry: AuditEntry) => void;
export interface AuditEntry {
timestamp: string;
jobId: string;
action: 'POLL_START' | 'STATUS_CHECK' | 'CHECKPOINT_UPDATE' | 'ABORT_TRIGGERED' | 'POLL_COMPLETE';
status: string;
latencyMs: number;
error?: string;
}
export function createAuditLogger(jobId: string): AuditLogCallback {
return (entry: AuditEntry) => {
const logLine = JSON.stringify({
...entry,
timestamp: new Date().toISOString(),
governance: true
});
console.log(`[AUDIT] ${logLine}`);
};
}
Step 5: Complete Job Poller Class
The poller ties validation, atomic GET operations, quota enforcement, and callbacks into a single executable module. It respects checkpoint intervals, triggers automatic cancellation on failure thresholds, and safely iterates until completion.
export class CognigyTrainingPoller {
private validator: PollValidator;
private auth: CognigyAuthClient;
private auditLogger: AuditLogCallback;
constructor(auth: CognigyAuthClient, jobId: string) {
this.validator = new PollValidator();
this.auth = auth;
this.auditLogger = createAuditLogger(jobId);
}
async poll(
config: PollConfig,
onStatusUpdate: StatusCallback
): Promise<void> {
this.auditLogger({
jobId: config.jobId,
action: 'POLL_START',
status: 'INITIATED',
latencyMs: 0
});
const controller = new AbortController();
let consecutiveFailures = 0;
for (const interval of config.checkpointIntervals) {
if (!this.validator.checkQuota()) {
await new Promise(resolve => setTimeout(resolve, 10000));
continue;
}
try {
const token = await this.auth.getToken();
const start = Date.now();
const statusData = await fetchTrainingStatus(config.jobId, token, controller.signal);
const latency = Date.now() - start;
this.validator.recordLatency(latency);
this.validator.recordCheckpoint(true);
this.auditLogger({
jobId: config.jobId,
action: 'STATUS_CHECK',
status: statusData.status,
latencyMs: latency
});
onStatusUpdate(config.jobId, statusData.status, statusData.progress, statusData.checkpoint);
consecutiveFailures = 0;
if (statusData.status === 'COMPLETED' || statusData.status === 'CANCELLED') {
this.auditLogger({
jobId: config.jobId,
action: 'POLL_COMPLETE',
status: statusData.status,
latencyMs: latency
});
return;
}
if (statusData.status === 'FAILED' || consecutiveFailures >= config.abortOnThreshold) {
await this.triggerCancellation(config.jobId, controller);
return;
}
await new Promise(resolve => setTimeout(resolve, interval));
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
this.validator.recordCheckpoint(false);
if (errorMessage.includes('RATE_LIMITED') || errorMessage.includes('SERVER_ERROR')) {
await new Promise(resolve => setTimeout(resolve, 2000 * Math.pow(2, consecutiveFailures)));
consecutiveFailures++;
} else if (errorMessage.includes('ABORT') || controller.signal.aborted) {
this.auditLogger({
jobId: config.jobId,
action: 'ABORT_TRIGGERED',
status: 'ABORTED',
latencyMs: 0,
error: errorMessage
});
return;
} else {
throw err;
}
}
}
this.auditLogger({
jobId: config.jobId,
action: 'POLL_COMPLETE',
status: 'CHECKPOINTS_EXHAUSTED',
latencyMs: 0
});
}
private async triggerCancellation(jobId: string, controller: AbortController): Promise<void> {
try {
const token = await this.auth.getToken();
await fetch(`${COGNIGY_BASE_URL}/api/v2/nlu/training/${jobId}/cancel`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
controller.abort();
this.auditLogger({
jobId,
action: 'ABORT_TRIGGERED',
status: 'CANCELLED_BY_POLLER',
latencyMs: 0
});
} catch (cancelErr) {
console.error('Cancellation failed:', cancelErr);
}
}
getMetrics() {
return this.validator.getMetrics();
}
}
Complete Working Example
The following script demonstrates end-to-end usage. Replace environment variables with your Cognigy.AI credentials and a valid training job UUID.
import { CognigyAuthClient } from './auth';
import { validatePollConfig, CognigyTrainingPoller, StatusCallback } from './poller';
async function main() {
const auth = new CognigyAuthClient();
const rawConfig = {
jobId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
checkpointIntervals: [5000, 10000, 15000, 20000],
abortOnThreshold: 3,
maxRetries: 2
};
const config = validatePollConfig(rawConfig);
const onStatusUpdate: StatusCallback = (jobId, status, progress, checkpoint) => {
console.log(`[ORCHESTRATION] Job ${jobId} -> ${status} | Progress: ${progress}% | Checkpoint: ${checkpoint}`);
};
const poller = new CognigyTrainingPoller(auth, config.jobId);
try {
await poller.poll(config, onStatusUpdate);
console.log('[POLLER] Polling cycle finished. Metrics:', poller.getMetrics());
} catch (error) {
console.error('[POLLER] Fatal error during polling:', error);
process.exit(1);
}
}
main();
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token has expired, the client credentials are incorrect, or the
cognigy.ai:api:readscope is missing. - Fix: Verify environment variables. The
CognigyAuthClientautomatically refreshes tokens, but if the initial exchange fails, check the client secret. Ensure the token buffer ingetToken()is not overriding a valid token. - Code: The auth client throws
Token acquisition failedwith the exact HTTP status. Log the response body to verify scope rejection.
Error: 429 Too Many Requests
- Cause: Polling frequency exceeds Cognigy.AI rate limits. The checkpoint interval matrix contains values below 5000ms, or concurrent pollers hit the same quota window.
- Fix: Enforce
MIN_INTERVAL_MSin the Zod schema. ThePollValidatortracks a 60-second sliding window and pauses requests whenmaxRequestsPerWindowis reached. Implement exponential backoff in the catch block. - Code: The error classifier returns
RETRYfor 429. The poller applies2000 * Math.pow(2, consecutiveFailures)delay before resuming.
Error: 400 Bad Request or Payload Mismatch
- Cause: The job UUID is invalid, the training job does not exist, or the API response structure changed.
- Fix: Validate the UUID format before instantiation. The
fetchTrainingStatusfunction performs strict format verification againstTrainingStatusResponse. If the response lacks thestatusfield, it throwsPAYLOAD_MISMATCH. - Code: Catch the error and verify the job exists via
GET /api/v2/nlu/modelsbefore polling.
Error: 503 Service Unavailable
- Cause: Cognigy.AI AI training service is scaling or undergoing maintenance.
- Fix: Classify as
RETRY. The poller waits and retries. If consecutive failures exceedabortOnThreshold, the poller triggers automatic cancellation to prevent orphaned jobs. - Code: The
classifyErrormethod maps 503 toRETRY. The poll loop incrementsconsecutiveFailuresand applies backoff.