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 theJobRegistry. 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 toexecute. Provides the payload, job metadata, a scoped logger, and lifecycle event emission.ScheduledJob— a queued job instance. Created whenruntime.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.loggeris not a full UDIOT logger. It is a simpleJobLoggerinterface. The backing implementation delegates to whatever logger was passed toBackgroundJobRuntime. By default this isconsole.JobContext.metadatamerges the definition-levelmetadataobject with the per-enqueuemetadataoption. Definition-level metadata is set once at decoration time; enqueue-level metadata is dynamic.
Pitfalls
attemptstarts at1, not0. The maximum number of attempts (including the first) isJobRetryPolicy.maxAttempts.payloadisundefinedif no payload was provided when the job was enqueued. Always use optional chaining (context.payload?.field) or provide a typed default.startedAtonJobContextis declared in the interface but not currently set byBackgroundJobRuntime. Rely onenqueuedAtfor timing calculations and measure elapsed time insideexecutemanually.