Many Narrative operations are asynchronous, returning a job ID that you can use to track progress. This guide covers how to monitor jobs and handle their completion.
Need real-time notifications instead of polling? Webhook subscriptions push job state changes to your endpoint the moment they happen.
Prerequisites
SDK installed and configured (see SDK Quickstart )
An API key with appropriate permissions
Understanding jobs
Operations like file uploads, dataset refreshes, and certain queries run asynchronously. When you start one of these operations, you receive a job ID that you can poll for status updates.
Job states
State Description pendingJob is queued and waiting to start runningJob is currently executing completedJob finished successfully pending_cancellationJob is marked for cancellation but still running cancelledJob was cancelled before completion failedJob encountered an error
Getting job status
Retrieve the status of a specific job:
import { NarrativeApi } from '@narrative.io/data-collaboration-sdk-ts' ;
const api = new NarrativeApi ({
apiKey: process . env . NARRATIVE_API_KEY ,
});
const job = await api . getJob ( 'job-id-here' );
console . log ( 'Job ID:' , job . id );
console . log ( 'State:' , job . state );
console . log ( 'Created:' , job . created_at );
console . log ( 'Updated:' , job . updated_at );
Listing jobs
List all jobs, optionally filtered by state, type, or data plane:
// List all jobs
const allJobs = await api . getJobs ();
console . log ( `Total jobs: ${ allJobs . total_records } ` );
// List with pagination
const paginatedJobs = await api . getJobs ({
page: 1 ,
per_page: 50 ,
});
// Filter and process
for ( const job of paginatedJobs . records ) {
console . log ( ` ${ job . id } : ${ job . state } - ${ job . type } ` );
}
The list response includes pagination metadata alongside records:
Field Description recordsThe jobs on this page current_pageThe 1-indexed page number returned total_pagesTotal number of pages available for this query total_recordsTotal number of jobs that match the query prev_pagePrevious page number, or null on the first page next_pageNext page number, or null on the last page
per_page is capped at 500. Requests with a larger value are rejected with a 400.
Filtering by state, type, and tag
The state, type, and tag query parameters are repeatable — pass each one multiple times to match any of several values. This is useful for building active-vs-finished views or scoping to a job family.
// Only jobs that are currently active
const active = await api . getJobs ({
state: [ 'pending' , 'running' , 'pending_cancellation' ],
});
// All jobs for a given dataset on a specific data plane
const datasetJobs = await api . getJobs ({
dataset_id: 'dataset-id' ,
data_plane_id: 'dp_your_data_plane_id' ,
});
Job responses always include a data_plane_id. Jobs that target the default Narrative-managed data plane report its id explicitly rather than returning null.
Polling for completion
A common pattern is to poll until a job completes:
import { NarrativeApi } from '@narrative.io/data-collaboration-sdk-ts' ;
const api = new NarrativeApi ({
apiKey: process . env . NARRATIVE_API_KEY ,
});
async function waitForJob ( jobId : string , maxWaitMs = 300000 ) {
const startTime = Date . now ();
const pollInterval = 5000 ; // 5 seconds
while ( Date . now () - startTime < maxWaitMs ) {
const job = await api . getJob ( jobId );
console . log ( `Job ${ jobId } : ${ job . state } ` );
if ( job . state === 'completed' ) {
return { success: true , job };
}
if ( job . state === 'failed' ) {
return { success: false , job , error: job . failures };
}
if ( job . state === 'cancelled' ) {
return { success: false , job , error: 'Job was cancelled' };
}
// Wait before next poll
await new Promise ( resolve => setTimeout ( resolve , pollInterval ));
}
throw new Error ( `Job ${ jobId } did not complete within ${ maxWaitMs } ms` );
}
// Usage
const result = await waitForJob ( 'job-id-here' );
if ( result . success ) {
console . log ( 'Job completed successfully' );
} else {
console . error ( 'Job failed:' , result . error );
}
Exponential backoff
For long-running jobs, use exponential backoff to reduce API calls:
async function waitForJobWithBackoff ( jobId : string , maxWaitMs = 600000 ) {
const startTime = Date . now ();
let pollInterval = 1000 ; // Start with 1 second
const maxInterval = 30000 ; // Cap at 30 seconds
while ( Date . now () - startTime < maxWaitMs ) {
const job = await api . getJob ( jobId );
console . log ( `Job ${ jobId } : ${ job . state } (next check in ${ pollInterval } ms)` );
if ( job . state === 'completed' ) {
return { success: true , job };
}
if ( job . state === 'failed' || job . state === 'cancelled' ) {
return { success: false , job };
}
// Wait with exponential backoff
await new Promise ( resolve => setTimeout ( resolve , pollInterval ));
pollInterval = Math . min ( pollInterval * 1.5 , maxInterval );
}
throw new Error ( `Job ${ jobId } timed out` );
}
Handling job results
Different job types return different results:
NQL query jobs
const queryResult = await api . getNqlByJobId ( jobId );
if ( queryResult . state === 'completed' ) {
console . log ( 'Rows returned:' , queryResult . result . rows );
console . log ( 'Query cost:' , queryResult . result . cost );
}
Dataset refresh jobs
const job = await api . getJob ( refreshJobId );
if ( job . state === 'completed' ) {
console . log ( 'Materialized view refreshed' );
// Fetch updated dataset statistics
const stats = await api . getStatistics ( datasetId );
}
Model inference jobs
import type { ModelInferenceRunResult } from '@narrative.io/data-collaboration-sdk-ts' ;
const job = await api . getJob ( inferenceJobId );
if ( job . state === 'completed' && job . result ) {
const result = job . result as ModelInferenceRunResult ;
console . log ( 'Tokens used:' , result . usage . total_tokens );
console . log ( 'Output:' , result . structured_output );
}
For inference-specific patterns, see Running Model Inference .
Error handling
Handle job failures gracefully:
async function executeWithRetry ( operation : () => Promise < string >, maxRetries = 3 ) {
for ( let attempt = 1 ; attempt <= maxRetries ; attempt ++ ) {
try {
const jobId = await operation ();
const result = await waitForJob ( jobId );
if ( result . success ) {
return result . job ;
}
console . error ( `Attempt ${ attempt } failed:` , result . error );
if ( attempt < maxRetries ) {
const backoff = Math . pow ( 2 , attempt ) * 1000 ;
console . log ( `Retrying in ${ backoff } ms...` );
await new Promise ( resolve => setTimeout ( resolve , backoff ));
}
} catch ( error ) {
console . error ( `Attempt ${ attempt } error:` , error );
if ( attempt === maxRetries ) {
throw error ;
}
}
}
throw new Error ( 'All retry attempts failed' );
}
// Usage
const job = await executeWithRetry ( async () => {
const result = await api . refreshMaterializedView ( datasetId );
return result . id ;
});
Batch job monitoring
Monitor multiple jobs concurrently:
async function waitForAllJobs ( jobIds : string []) {
const results = await Promise . all (
jobIds . map ( async ( jobId ) => {
try {
const result = await waitForJob ( jobId );
return { jobId , ... result };
} catch ( error ) {
return { jobId , success: false , error };
}
})
);
const succeeded = results . filter ( r => r . success );
const failed = results . filter ( r => ! r . success );
console . log ( `Completed: ${ succeeded . length } / ${ results . length } ` );
console . log ( `Failed: ${ failed . length } / ${ results . length } ` );
return { results , succeeded , failed };
}
// Usage
const jobIds = [ 'job-1' , 'job-2' , 'job-3' ];
const { succeeded , failed } = await waitForAllJobs ( jobIds );
Best practices
Practice Description Use exponential backoff Reduce API calls for long-running jobs Set reasonable timeouts Prevent indefinite waiting Log progress Track job state changes for debugging Handle all states Account for all terminal states: completed, failed, and cancelled Implement retries Retry failed operations when appropriate
Troubleshooting
Issue Cause Solution Job stays pending Queue backlog or resource constraints Wait longer or contact support Job fails immediately Invalid input or permissions Check error details and fix input Timeout waiting Job taking longer than expected Increase timeout or check job complexity 404 on job ID Job expired or invalid ID Verify job ID and check retention
Related content
Job Types Reference for all job types
Error Handling Handle SDK errors gracefully
Uploading Data Upload files and track ingestion
Managing Datasets Dataset operations that create jobs