A powerful workflow management system built on fibjs for orchestrating complex task dependencies and distributed task execution.
- Workflow Management: Parent-child task relationships, automatic state propagation
- Task Types: Async tasks and cron jobs with priorities and delays
- Cron Execution Modes:
cronMode: 'spawn'gives every cron round its ownasyncrun row, so a suspending or failing round never freezes the schedule - Idempotent Scheduling:
cron()upserts per(name, cronKey), so restarts and multi-instance boots never duplicate triggers - Schedule Control:
cancelCron()/cancelTask()stop schedules without pretending they failed;cancelTasks()does the same for a filtered batch of task rows;getCronSchedules()andpruneCronSchedules()expose orphaned rows - Per-Schedule Time Zones: IANA
timezoneper cron job, validated at registration - Catch-Up Policy:
catchUp: 'once'runs a missed window,'skip'advances past it with an audit trail - State Management: Comprehensive task lifecycle and state transitions
- Human-in-the-Loop: Explicit task suspension (
task.suspend()) for external interaction (e.g. approval), resumed later viaresumeTask/cancelTaskwith resume data - Hot Reload: Update or remove handlers at runtime without restarting workers
- Reliability: Automatic retries, timeout protection, transaction safety
- Retention: Global window plus per-row
retention_expire_timeoverrides - Worker Recovery: Reclaim
runningjobs from dead or superseded workers through thefib_flow_workersregistry - Execution Audit: Persisted task events, attempts, workflow timelines, and handler checkpoints
- Database Support: SQLite/MySQL/PostgreSQL with flexible connection options
- Resource Management: Load balancing and specialized worker support
fibjs --install fib-flowconst { TaskManager } = require('fib-flow');
// Initialize task manager with an explicit backend
const taskManager = new TaskManager({
dbConnection: 'sqlite::memory:',
pod_id: 'scheduler-a',
worker_heartbeat_interval: 1000,
worker_heartbeat_timeout: 5000,
recover_running_jobs: true
});
taskManager.db.setup();
// Basic task handler
taskManager.use('sendEmail', async (task) => {
const { to, subject, body } = task.payload;
if (typeof to !== 'string' || typeof subject !== 'string' || typeof body !== 'string') {
throw new Error('Invalid email payload');
}
task.audit('payload_validated', {
message: 'Email payload validated',
metadata: { recipient: to }
});
task.progress('Sending email', {
stage_name: 'delivery',
progress_percent: 80
});
await sendEmail(to, subject, body);
return { sent: true };
});
// Handler with configuration
taskManager.use('processImage', {
handler: async (task) => {
const { path } = task.payload;
await processImage(path);
return { processed: true };
},
timeout: 120, // 2 minutes timeout
max_retries: 2, // Maximum 2 retries
retry_interval: 30 // 30 seconds retry interval
});
// Start processing
taskManager.start();
// Add a task
taskManager.async('sendEmail', {
to: 'user@example.com',
subject: 'Hello',
body: 'World'
});
// Query task audit history with pagination metadata
const taskAudit = taskManager.getTaskAudit(1, {
events: { limit: 20, order: 'asc' },
attempts: { limit: 10, order: 'asc' }
});
// Query workflow-level aggregate audit summary
const workflowSummary = taskManager.getWorkflowAuditSummary(1);
// Run explicit retention cleanup when needed
taskManager.runRetention();
console.log(workflowSummary.stage_timings);
console.log(workflowSummary.critical_path);
// Hot update a handler for future claims
taskManager.use('sendEmail', async (task) => {
const { to, subject, body } = task.payload;
await sendEmailV2(to, subject, body);
return { sent: true, provider: 'v2' };
});
// Remove a handler when a flow is unloaded
taskManager.unuse('processImage');
// Explicit suspension for human interaction (e.g. approval)
taskManager.use('requestApproval', async (task) => {
if (task.stage === 0) {
await notifyApprover(task.payload);
return task.suspend({ reason: 'awaiting_approval' });
}
// Resumed run: stage has advanced; interaction results live in the caller's store
return { approved: true };
});
// External system resumes the task when the interaction finishes
taskManager.resumeTask(taskId);
// Or rejects it permanently
// taskManager.cancelTask(taskId, { reason: 'request abandoned' });
// List all tasks waiting for approval
const pendingApprovals = taskManager.getTasksByStatus('suspended', {
suspend_reason: 'awaiting_approval'
});Schedule management:
// Idempotent: re-registering the same (name, cronKey) updates the existing row
taskManager.cron('nightly_report', '0 3 * * *', payload, {
cronKey: 'default',
timezone: 'Asia/Shanghai',
catchUp: 'skip',
retention_expire_time: 90 * 86400
});
// Inspect schedules, including rows whose handler is no longer registered
const schedules = taskManager.getCronSchedules();
const stats = taskManager.getCronScheduleStats(); // { total, active, paused, cancelled, orphan }
// Stop a schedule deliberately (recorded as `cancelled`, not as a failure)
taskManager.cancelCron({ name: 'nightly_report' });
// Reclaim schedules whose handler was removed
taskManager.pruneCronSchedules();Cron execution modes:
// Default ('inline'): the cron row runs the handler and resets itself each round.
// A refreshing dashboard is a typical fit — there is nothing to suspend or audit
// per round.
const inline = new TaskManager({ dbConnection: 'sqlite:jobs.db' });
// 'spawn': the cron row never runs business code. Each round inserts an
// independent async run row and advances the schedule in one transaction, so a
// round can suspend, retry, be audited and be collected on its own.
const spawn = new TaskManager({ dbConnection: 'sqlite:jobs.db', cronMode: 'spawn' });
spawn.use('nightly_report', (task) => task.stage === 0
? task.suspend({ reason: 'awaiting_approval' }) // this round parks…
: publish(task.payload)); // …the schedule keeps ticking
spawn.cron('nightly_report', '0 3 * * *', { region: 'eu' });In spawn mode: the schedule row stays pending forever, result is never
overwritten, no attempt or claim-event rows accumulate on it, handler
max_concurrent_tasks gates runs rather than the tick, and
getCronSchedules()[].last_spawned_task_id points at the latest run.
runCronNow(scheduleId) spawns one extra run without moving next_run_time.
Two behaviours differ from inline mode and are worth knowing before switching:
- Rounds do not overlap by default. Inline was mutually exclusive for free
(its row was
running, so not claimable). Spawn keeps that guarantee viamaxInstances(default1): a round that would exceed it is skipped, withnext_run_timestill advancing and atask_cron_overlap_skippedevent recorded. SetmaxInstances: 0to allow unbounded overlap — a handler slower than its cron period will then stack runs that can never be collected. - Failures surface on the schedule row's
error, not itsstatus. The schedule keeps firing, so it stayspending;getCronSchedules()[].last_errorcarries the latest run's failure and a later success clears it. When deciding "is this schedule healthy?", readlast_error, not onlystatus.
Hot reload semantics:
- A task attempt that is already executing keeps the handler version captured when it started.
- A paused or suspended task that resumes later uses the latest registered handler.
- Child tasks created by a running parent resolve against the live handler registry when they are created.
Worker recovery semantics:
worker_ididentifies a single running process instance.pod_ididentifies a stable logical node across restarts.- When
pod_idis configured, fib-flow records worker liveness infib_flow_workers. - If a worker becomes dead or is superseded by a newer worker from the same
pod_id, itsrunningtasks can be reclaimed without waiting for task timeout. - Running-task updates are ownership-fenced by
worker_id, so stale workers cannot safely write back after recovery.
The package ships declarations out of the box ("types": "index.d.ts"), so no
@types/* package is needed:
import { TaskManager, type TaskRecord, type CronOptions } from 'fib-flow';
const taskManager = new TaskManager({ dbConnection: 'sqlite::memory:' });
taskManager.db.setup();
taskManager.cron('nightly_report', '0 3 * * *', { region: 'eu' }, {
cronKey: 'eu',
timezone: 'Asia/Shanghai',
catchUp: 'skip'
});
const task: TaskRecord | null = taskManager.getTask(1);What the declarations cover:
TaskManager— every public method with its real return type (resumeTask,pauseTaskandcancelTaskreturn the updated row; the query APIs return typed rows and paged results).- Handler contract —
TaskHandlerTask<T>typestask.payloadfrom the handler's generic and includescheckTimeout(),audit(),progress()andsuspend(); the second argument is the typednextsub-task factory. - Options —
TaskManagerOptions,TaskOptions,CronOptions(cronKey,timezone,catchUp,retention_expire_time). - Records —
TaskRecord,TaskEventRecord,TaskAttemptRecord,WorkerRecord,CronSchedule,CronScheduleStats,WorkflowAuditSummary, and the retention/result shapes. - Adapter surface —
DatabaseAdapterpluscreateAdapter()and the three engine classes.
Keeping declarations honest is part of the build: fibjs --check type-checks the
declarations (skipLibCheck: false) together with test/types/contract.ts, a
compile-time contract test full of positive and @ts-expect-error assertions. A
drift between runtime and declaration fails the check with a non-zero exit code.
- API Documentation
- TypeScript Support
- Retention Policy
- Execution Audit Events
- Cron Syntax Guide
- Usage Examples
- Common Use Cases
- Background Processing: File processing, report generation
- Scheduled Tasks: Data synchronization, backups
- Complex Workflows: Multi-step data pipelines
- Distributed Systems: Task coordination across services
For detailed examples and implementation guides, see Usage Examples.
MIT License