Skip to main content
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.
For a detailed list of job types, see Job Types Reference.
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

StateDescription
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:
FieldDescription
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

PracticeDescription
Use exponential backoffReduce API calls for long-running jobs
Set reasonable timeoutsPrevent indefinite waiting
Log progressTrack job state changes for debugging
Handle all statesAccount for all terminal states: completed, failed, and cancelled
Implement retriesRetry failed operations when appropriate

Troubleshooting

IssueCauseSolution
Job stays pendingQueue backlog or resource constraintsWait longer or contact support
Job fails immediatelyInvalid input or permissionsCheck error details and fix input
Timeout waitingJob taking longer than expectedIncrease timeout or check job complexity
404 on job IDJob expired or invalid IDVerify job ID and check retention

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