Background Job Decorators
Two decorators define a background job and its schedule. @AIBackgroundJob registers the class in the global JobRegistry and sets execution metadata. @AISchedule attaches a schedule independently, or can be applied alongside @AIBackgroundJob to separate concerns.
reflect-metadata must be imported once in your entry point when using these decorators.
@AIBackgroundJob(options) — Class Decorator
Registers a class as a background job and stores its JobDefinition in the global registry. The class must implement a execute(context: JobContext<TPayload>): Promise<void> method.
import { AIBackgroundJob } from '@tagna/udiot';
@AIBackgroundJob({
name?: string; // Job name used for lookup; defaults to class name
description?: string; // Human-readable description
category?: JobCategory; // 'ai' | 'maintenance' | 'data' | 'system' | 'custom'; default 'custom'
priority?: JobPriority; // 'low' | 'normal' | 'high' | 'critical'; default 'normal'
retry?: Partial<JobRetryPolicy>; // Retry behaviour (see JobRetryPolicy below)
schedule?: JobScheduleMetadata; // Inline schedule (alternative to @AISchedule)
tenantScope?: JobTenantScope; // 'global' | 'tenant'; default 'global'
tags?: string[]; // Arbitrary labels
metadata?: Record<string, unknown>; // Extra data passed through to JobContext.metadata
})
class MyJob { /* … */ }
When name is omitted, the class name is used verbatim. Use unique names across your application; re-registering the same name overwrites the previous definition with a console warning.
@AISchedule(options) — Class Decorator
Attaches schedule metadata to a job class. Can be applied on its own or before @AIBackgroundJob. The schedule is stored via the registry's upsertJobSchedule function and picked up by JobScheduler at start time.
import { AISchedule } from '@tagna/udiot';
@AISchedule({
kind: JobScheduleKind; // 'cron' | 'interval' | 'immediate' | 'auto' (required)
expression?: string; // Cron expression (required when kind === 'cron')
intervalMs?: number; // Milliseconds between runs (required when kind === 'interval')
startAt?: number; // Unix timestamp; delay first interval run until this time
timezone?: string; // IANA timezone string for cron evaluation, e.g. 'America/New_York'
aiDescription?: string; // Natural-language description of when the job runs
})
class MyJob { /* … */ }
Enum and Type Reference
JobCategory
type JobCategory = 'ai' | 'maintenance' | 'data' | 'system' | 'custom';
JobPriority
type JobPriority = 'low' | 'normal' | 'high' | 'critical';
JobTenantScope
type JobTenantScope = 'global' | 'tenant';
'global' — the job runs once across the entire system.
'tenant' — the job runs per tenant; use context.tenantId inside execute() to scope work.
JobScheduleKind
type JobScheduleKind = 'cron' | 'interval' | 'immediate' | 'auto';
| Kind | Behaviour |
|---|---|
'cron' |
Fires on a cron expression. Requires expression. Parsed by cron-parser. |
'interval' |
Fires every intervalMs milliseconds after the previous run (or after startAt). |
'immediate' |
Fires once as soon as JobScheduler starts. |
'auto' |
Uses intervalMs if provided, otherwise defaults to 1 hour. |
JobRetryPolicy
interface JobRetryPolicy {
strategy: 'none' | 'fixed' | 'exponential';
maxAttempts?: number; // Total attempts including the first; default 3
initialDelayMs?: number; // Delay before the first retry; default 1000
maxDelayMs?: number; // Cap on exponential growth; default 30000
jitter?: boolean; // Add random jitter to delay; default false
}
Default when retry is omitted from @AIBackgroundJob:
{ strategy: 'fixed', maxAttempts: 3, initialDelayMs: 1000, maxDelayMs: 30000 }
JobScheduleMetadata
The resolved schedule stored in the registry and read by JobScheduler.
interface JobScheduleMetadata {
kind: JobScheduleKind;
expression?: string; // Cron expression string
intervalMs?: number; // Milliseconds between runs
startAt?: number; // Unix timestamp for first interval run
timezone?: string; // IANA timezone
aiDescription?: string; // Natural-language description
}
API Reference
| Export | Signature | Description |
|---|---|---|
AIBackgroundJob |
(options?: AIBackgroundJobOptions) => ClassDecorator |
Register a job class |
AISchedule |
(options: AIScheduleOptions) => ClassDecorator |
Attach a schedule to a job class |
getScheduleMetadata |
(target: object) => JobScheduleMetadata | undefined |
Read schedule metadata from a class |
Basic Usage Example
import 'reflect-metadata';
import {
AIBackgroundJob,
AISchedule,
JobContext,
} from '@tagna/udiot';
interface CleanupPayload {
olderThanDays: number;
}
@AISchedule({
kind: 'cron',
expression: '0 2 * * *', // Every day at 02:00 UTC
timezone: 'UTC',
aiDescription: 'Daily cleanup of old email records at 2 AM UTC',
})
@AIBackgroundJob({
name: 'EmailCleanupJob',
description: 'Removes email records older than the configured threshold',
category: 'maintenance',
priority: 'low',
retry: {
strategy: 'exponential',
maxAttempts: 5,
initialDelayMs: 2000,
maxDelayMs: 60_000,
jitter: true,
},
tags: ['email', 'cleanup'],
})
class EmailCleanupJob {
async execute(context: JobContext<CleanupPayload>): Promise<void> {
const { olderThanDays = 90 } = context.payload ?? {};
context.logger.info('Starting email cleanup', { olderThanDays });
// … perform cleanup …
context.logger.info('Email cleanup complete');
}
}
Common Patterns
Immediate one-shot job
@AISchedule({ kind: 'immediate' })
@AIBackgroundJob({ name: 'SeedDatabaseJob', category: 'system' })
class SeedDatabaseJob {
async execute(context: JobContext): Promise<void> {
context.logger.info('Seeding database…');
// … seed logic …
}
}
Interval job with delayed start
@AISchedule({
kind: 'interval',
intervalMs: 5 * 60 * 1000, // Every 5 minutes
startAt: Date.now() + 60 * 1000, // Start 1 minute after the scheduler boots
})
@AIBackgroundJob({ name: 'MetricsAggregatorJob', category: 'system' })
class MetricsAggregatorJob {
async execute(context: JobContext): Promise<void> { /* … */ }
}
Tenant-scoped job
@AIBackgroundJob({
name: 'TenantReportJob',
category: 'data',
tenantScope: 'tenant',
})
class TenantReportJob {
async execute(context: JobContext): Promise<void> {
const { tenantId } = context;
context.logger.info(`Generating report for tenant ${tenantId}`);
// … generate report for this tenant only …
}
}
Integration Notes
- Decorator execution order in TypeScript is bottom-up.
@AIBackgroundJobreads the schedule stored by@AISchedule, so@AISchedulemust appear above@AIBackgroundJobin source code. - If you provide
scheduledirectly inside@AIBackgroundJoboptions,@AIScheduleis not needed. Inline schedules and decorator-based schedules are merged, with the inline schedule taking precedence. @AIBackgroundJobcallsregisterJobDefinitionwhich stores the definition globally at import time. If the same module is imported multiple times (e.g., with Jest module isolation turned off), the definition may be registered twice. UseresetJobRegistry()inbeforeEachto keep tests clean.
Pitfalls
- A class decorated with
@AIBackgroundJobbut without anexecutemethod will causeBackgroundJobRuntimeto throw at execution time, not at decoration time. Add a runtime guard or use TypeScript'simplements JobHandler<TPayload>to catch this at compile time. - The
jitterflag inJobRetryPolicyis stored in the definition but the built-in runtime does not yet apply jitter. It is available for custom runtime implementations. @AISchedulealone (without@AIBackgroundJob) still callsupsertJobSchedule, but without aJobDefinitionthe scheduler cannot enqueue the job. Always pair@AISchedulewith@AIBackgroundJob.