@happyvertical/smrt-jobs
Background job execution with persistent queue, retry strategies, cron scheduling, and a fluent JobBuilder API.
v0.20.44Task RunnerSchedulerFluent API
Overview
smrt-jobs provides background job execution for any SmrtObject method.
Jobs are persisted in the _smrt_jobs system table, processed by a polling-based
TaskRunner with concurrency control, and can be scheduled via cron expressions through the
ScheduleRunner.
Installation
bash
npm install @happyvertical/smrt-jobsQuick Start
typescript
import { withBackgroundJobs, TaskRunner } from '@happyvertical/smrt-jobs';
import { Document } from './document.js';
// Mixin adds .bg() and .background() to any SmrtObject class
const BackgroundDocument = withBackgroundJobs(Document);
const doc = new BackgroundDocument({ db });
await doc.initialize();
// Quick enqueue -- runs when a TaskRunner picks it up
const handle = await doc.bg('generateSummary', { format: 'md' });
// Fluent builder for advanced options
const handle2 = await doc.background('generateSummary', { format: 'md' })
.delay('5m')
.priority('high')
.retries(5)
.queue('analysis')
.timeout(600000)
.enqueue();
// Wait for result (polling-based)
const result = await handle2.wait({ timeout: 60000, pollInterval: 100 });Core Models
SmrtJob
typescript
class SmrtJob extends SmrtObject {
queue: string // 'default' by default
objectType: string // Class name for ObjectRegistry lookup
objectId: string
method: string // Method to call on the object
args: string // JSON arguments
runAt: Date
priority: number // Higher = sooner
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
attempts: number
maxAttempts: number
timeout: number // Default 5 minutes (ms)
retryStrategy: string
workerId?: string
workerHeartbeat?: Date
resultPointer?: string // App-defined result storage
}TaskRunner
typescript
const runner = new TaskRunner({
concurrency: 5, // Max parallel jobs
pollInterval: 1000, // Poll every 1 second
heartbeatInterval: 30000, // Heartbeat every 30 seconds
shutdownTimeout: 30000, // Graceful shutdown timeout
queues: ['default', 'analysis'],
});
await runner.initialize(db);
await runner.start();
// Listen for events
runner.on('job:completed', (job, result) => { /* ... */ });
runner.on('job:failed', (job, error) => { /* ... */ });
// Graceful shutdown
process.on('SIGTERM', () => runner.stop());Cron Scheduling
typescript
import { ScheduleRunner } from '@happyvertical/smrt-jobs';
// Polls _smrt_agent_schedules for due cron entries
const scheduleRunner = new ScheduleRunner({ pollInterval: 30000 });
await scheduleRunner.initialize(db);
await scheduleRunner.start();
// Connect TaskRunner events to update schedule state
taskRunner.on('job:completed', (job) => {
const scheduleId = job.args?._scheduleId;
if (scheduleId) scheduleRunner.handleJobCompletion(scheduleId, true);
});
taskRunner.on('job:failed', (job, error) => {
const scheduleId = job.args?._scheduleId;
if (scheduleId) scheduleRunner.handleJobCompletion(scheduleId, false, error.message);
});
// Cron format: 5-field (minute hour dom month dow)
// Supports: *, ranges, lists, steps
// All times are UTC (not timezone-aware)Best Practices
DOs
- Use
withBackgroundJobs()mixin to add background capabilities - Use the fluent builder for complex job configuration
- Wire ScheduleRunner events to TaskRunner for completion tracking
- Implement graceful shutdown with
runner.stop() - Use priority levels for job ordering (critical/high/normal/low)
DON'Ts
- Don't forget to call
.enqueue()on the builder (it's lazy) - Don't assume timezone support (cron is UTC only)
- Don't expect a dead letter queue (failed jobs stay in DB)
- Don't rely on
resultPointerwithout implementing a result backend - Don't poll too aggressively with
handle.wait()(default 100ms is reasonable)