Docs/background jobsv1.0.0-rc.1

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. @AIBackgroundJob reads the schedule stored by @AISchedule, so @AISchedule must appear above @AIBackgroundJob in source code.
  • If you provide schedule directly inside @AIBackgroundJob options, @AISchedule is not needed. Inline schedules and decorator-based schedules are merged, with the inline schedule taking precedence.
  • @AIBackgroundJob calls registerJobDefinition which 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. Use resetJobRegistry() in beforeEach to keep tests clean.

Pitfalls

  • A class decorated with @AIBackgroundJob but without an execute method will cause BackgroundJobRuntime to throw at execution time, not at decoration time. Add a runtime guard or use TypeScript's implements JobHandler<TPayload> to catch this at compile time.
  • The jitter flag in JobRetryPolicy is stored in the definition but the built-in runtime does not yet apply jitter. It is available for custom runtime implementations.
  • @AISchedule alone (without @AIBackgroundJob) still calls upsertJobSchedule, but without a JobDefinition the scheduler cannot enqueue the job. Always pair @AISchedule with @AIBackgroundJob.
From the canonical UDIOT public documentation.Try it in the playground ↗