Docs/background jobsv1.0.0-rc.1

Job Types

This page documents the core TypeScript interfaces and types that describe background jobs, their execution context, and their results. Understanding these types is essential for implementing job handlers and interacting with the runtime programmatically.

Key Concepts

  • JobDefinition — the complete description of a job registered in the JobRegistry. Built automatically by @AIBackgroundJob, or constructed manually for programmatic use.
  • JobHandler<TPayload> — the interface a job class must implement. A single method: execute(context).
  • JobContext<TPayload> — the object passed to execute. Provides the payload, job metadata, a scoped logger, and lifecycle event emission.
  • ScheduledJob — a queued job instance. Created when runtime.scheduleJob() is called and stored by the provider until execution.

JobDefinition<TPayload>

interface JobDefinition<TPayload = unknown> {
  name: string;                       // Unique job name; used for lookup and display
  description?: string;
  target: JobConstructor<TPayload>;   // The class constructor; instantiated per execution
  category: JobCategory;              // 'ai' | 'maintenance' | 'data' | 'system' | 'custom'
  priority: JobPriority;              // 'low' | 'normal' | 'high' | 'critical'
  retry: JobRetryPolicy;              // Always present; defaults applied by the decorator
  schedule?: JobScheduleMetadata;     // Set when the job has a recurring schedule
  tenantScope?: JobTenantScope;       // 'global' | 'tenant'
  tags?: string[];
  metadata?: Record<string, unknown>; // Extra data forwarded to JobContext.metadata
  filePath?: string;                  // Source file path; set for debugging purposes
  lineNumber?: number;
}

JobConstructor<TPayload>

interface JobConstructor<TPayload = unknown> {
  new (...args: any[]): JobHandler<TPayload>;
}

The runtime calls new definition.target() on every execution. Job classes must have a no-argument constructor (or a constructor whose arguments are all optional).

JobHandler<TPayload>

The interface your job class must implement:

interface JobHandler<TPayload = unknown> {
  execute(context: JobContext<TPayload>): Promise<void> | void;
}

The method may return a Promise<void> or be async. Throwing an error causes the runtime to record a 'failed' lifecycle event and trigger the retry policy.

JobContext<TPayload>

Passed as the sole argument to execute. Provides everything a job needs at runtime.

interface JobContext<TPayload = unknown> {
  jobId: string;                         // Unique ID for this execution
  definition: JobDefinition<TPayload>;   // The job definition
  payload?: TPayload;                    // Data passed when the job was enqueued
  attempt: number;                       // 1-based attempt counter
  enqueuedAt: number;                    // Unix ms when the job was enqueued
  startedAt?: number;                    // Unix ms when execution started
  tenantId?: string;                     // Set for tenant-scoped jobs
  metadata?: Record<string, unknown>;    // Merged from definition.metadata + enqueue options
  trace?: JobTraceContext;               // OpenTelemetry trace context
  logger: JobLogger;                     // Scoped logger; prefixes messages with the job name
  emitLifecycle: (event: JobLifecycleEvent) => void; // Emit a custom lifecycle event
}

JobLogger

interface JobLogger {
  info(message: string, meta?: Record<string, unknown>): void;
  warn(message: string, meta?: Record<string, unknown>): void;
  error(message: string, meta?: Record<string, unknown>): void;
  debug?(message: string, meta?: Record<string, unknown>): void;
}

The logger provided in JobContext prefixes every message with [JobName]. debug is optional and may be undefined when the runtime is configured without a debug-capable logger.

JobTraceContext

interface JobTraceContext {
  traceId?: string;
  spanId?: string;
  parentSpanId?: string;
  sampled?: boolean;
  attributes?: Record<string, unknown>;
}

JobScheduleMetadata

Describes when and how often a job should run. Stored in the registry and read by JobScheduler.

interface JobScheduleMetadata {
  kind: 'cron' | 'interval' | 'immediate' | 'auto';
  expression?: string;    // Cron expression string; required when kind === 'cron'
  intervalMs?: number;    // Milliseconds between runs; required when kind === 'interval'
  startAt?: number;       // Unix timestamp; delays the first interval run
  timezone?: string;      // IANA timezone for cron parsing
  aiDescription?: string; // Natural-language schedule description
}

JobLifecycleEvent

Emitted automatically by the runtime at key points in a job's lifecycle, and available for custom emission via context.emitLifecycle.

interface JobLifecycleEvent {
  type: 'scheduled' | 'started' | 'completed' | 'failed' | 'retrying' | 'cancelled';
  jobName: string;
  jobId: string;
  timestamp: number;         // Unix ms
  attempt: number;
  payloadSummary?: string;
  error?: {
    message: string;
    stack?: string;
  };
  durationMs?: number;       // Set for 'completed' and 'failed'
  tenantId?: string;
}

The last 200 lifecycle events are kept in JobRegistry (accessible via getLifecycleHistory()). Events are also forwarded to jobTelemetry and to the JobRuntimeHooks.onLifecycle callback.

ScheduledJob

Represents a job that has been accepted by the provider and is waiting for execution.

interface ScheduledJob {
  id: string;
  definition: JobDefinition;
  payload?: unknown;
  scheduledAt: number;       // Unix ms when the job should run
  tenantId?: string;
  metadata?: Record<string, unknown>;
}

JobQueueStats

Returned by provider.getStats(). Both built-in providers implement this.

interface JobQueueStats {
  waiting: number;    // Jobs accepted but not yet executing
  active: number;     // Jobs currently executing
  completed: number;  // Total completed (since provider start)
  failed: number;     // Total failed (since provider start)
  delayed: number;    // Jobs scheduled for a future time
  since: number;      // Unix ms when stats tracking started
}

JobRuntimeHooks

Optional callbacks passed to BackgroundJobRuntime for observability.

interface JobRuntimeHooks {
  onLifecycle?: (event: JobLifecycleEvent) => void;
  onError?: (error: Error, context: JobContext) => void;
}

Defining a Job Without Decorators

You can register a job programmatically without using @AIBackgroundJob:

import {
  JobDefinition,
  JobContext,
  JobHandler,
  registerJobDefinition,
} from '@tagna/udiot';

interface InvoicePayload {
  invoiceId: string;
  amount: number;
}

class ProcessInvoiceJob implements JobHandler<InvoicePayload> {
  async execute(context: JobContext<InvoicePayload>): Promise<void> {
    const { invoiceId, amount } = context.payload!;
    context.logger.info(`Processing invoice ${invoiceId} for ${amount}`);
    // … payment processing logic …
  }
}

const definition: JobDefinition<InvoicePayload> = {
  name: 'ProcessInvoiceJob',
  description: 'Process a customer invoice payment',
  target: ProcessInvoiceJob,
  category: 'data',
  priority: 'high',
  retry: {
    strategy: 'exponential',
    maxAttempts: 4,
    initialDelayMs: 500,
    maxDelayMs: 15_000,
  },
  tenantScope: 'tenant',
  tags: ['billing', 'payments'],
};

registerJobDefinition(definition);

Then enqueue it at runtime:

import { BackgroundJobRuntime } from '@tagna/udiot';

const runtime = new BackgroundJobRuntime();
await runtime.start();

await runtime.scheduleJob<InvoicePayload>(
  'ProcessInvoiceJob',
  { invoiceId: 'inv_001', amount: 99.99 },
  { tenantId: 'tenant_abc' }
);

Common Patterns

Checking attempt number for conditional logic

async execute(context: JobContext): Promise<void> {
  if (context.attempt > 1) {
    context.logger.warn(`Retry attempt ${context.attempt}`);
  }
  // … job work …
}

Emitting a custom lifecycle event

async execute(context: JobContext<ReportPayload>): Promise<void> {
  context.emitLifecycle({
    type: 'started',
    jobName: context.definition.name,
    jobId: context.jobId,
    timestamp: Date.now(),
    attempt: context.attempt,
    payloadSummary: `reportId=${context.payload?.reportId}`,
  });
  // … report generation …
}

Integration Notes

  • JobContext.logger is not a full UDIOT logger. It is a simple JobLogger interface. The backing implementation delegates to whatever logger was passed to BackgroundJobRuntime. By default this is console.
  • JobContext.metadata merges the definition-level metadata object with the per-enqueue metadata option. Definition-level metadata is set once at decoration time; enqueue-level metadata is dynamic.

Pitfalls

  • attempt starts at 1, not 0. The maximum number of attempts (including the first) is JobRetryPolicy.maxAttempts.
  • payload is undefined if no payload was provided when the job was enqueued. Always use optional chaining (context.payload?.field) or provide a typed default.
  • startedAt on JobContext is declared in the interface but not currently set by BackgroundJobRuntime. Rely on enqueuedAt for timing calculations and measure elapsed time inside execute manually.
From the canonical UDIOT public documentation.Try it in the playground ↗