cbq
Use this skill when implementing asynchronous background job processing in ColdBox/BoxLang with cbq. Covers job class creation, queue configuration (database, Redis, SQS), dispatching jobs, delayed dispatch, queue routing, retry handling, worker pool management, and monitoring patterns.
CBQ Skill
When to Use This Skill
Load this skill when:
- Offloading slow tasks (email, PDF, API calls) to background workers
- Implementing retry logic for unreliable external integrations
- Routing different job types to different queue backends
- Scheduling delayed job execution
- Building worker consumers for database or Redis queues
Installation
box install cbq
Configuration
config/modules/cbq.cfc
function configure() {
return {
// Default queue connection
defaultConnection : "database",
connections : {
database : {
driver : "CBQDatabase",
dsn : "myDatasource",
tableName : "cbq_jobs",
queue : "default",
timeout : 60
},
redis : {
driver : "CBQRedis",
host : getSystemSetting( "REDIS_HOST", "127.0.0.1" ),
port : getSystemSetting( "REDIS_PORT", "6379" ),
queue : "default",
timeout: 60
}
},
// Worker configuration
workers : {
numThreads : 5,
sleep : 1, // seconds between polls
maxAttempts : 3 // global default
}
}
}
Create Database Table (cfmigrations)
function up( schema ) {
schema.create( "cbq_jobs", ( table ) => {
table.increments( "id" )
table.string( "queue" ).default( "default" )
table.string( "connection" ).default( "database" )
table.longText( "payload" )
table.unsignedTinyInteger( "attempts" ).default( 0 )
table.boolean( "reserved" ).default( false )
table.dateTime( "reserved_at" ).nullable()
table.dateTime( "available_at" )
table.dateTime( "created_at" )
} )
schema.create( "cbq_failed_jobs", ( table ) => {
table.increments( "id" )
table.string( "connection" )
table.string( "queue" )
table.longText( "payload" )
table.longText( "exception" )
table.dateTime( "failed_at" )
} )
}
Creating Jobs
// jobs/SendWelcomeEmailJob.bx
class extends="cbq.models.AbstractJob" {
// Inject services the job needs
property name="mailService" inject="MailService@myApp";
// Job payload properties
property name="userId" type="numeric";
property name="email" type="string";
// Optional — override defaults from config
this.queue = "emails"
this.connection = "default"
this.maxAttempts = 3
this.timeout = 30
function handle() {
var user = userService.get( userId )
mailService
.newMail(
to = email,
subject = "Welcome to the platform!",
type = "html"
)
.setBodyTemplate( "email/templates/welcome" )
.setBodyTokens( { name: user.getName() } )
.send()
}
// Called when all retries exhausted
function failed( exception ) {
log.error( "Failed to send welcome email to #email# after #this.maxAttempts# attempts: #exception.message#" )
}
}
Dispatching Jobs
property name="sendWelcomeEmailJob" inject="SendWelcomeEmailJob";
// Dispatch immediately
sendWelcomeEmailJob
.setUserId( newUser.getId() )
.setEmail( newUser.getEmail() )
.dispatch()
// Delay execution (seconds)
sendWelcomeEmailJob
.setUserId( rc.id )
.setEmail( rc.email )
.delay( 30 )
.dispatch()
// Specify queue and connection
sendWelcomeEmailJob
.setUserId( rc.id )
.setEmail( rc.email )
.onQueue( "high-priority" )
.onConnection( "redis" )
.dispatch()
Production Patterns
Handler — Dispatch on User Registration
function register( event, rc, prc ) {
var user = userService.create( rc )
// Dispatch async background jobs
sendWelcomeEmailJob.setUserId( user.getId() ).setEmail( user.getEmail() ).dispatch()
setupDefaultPreferencesJob.setUserId( user.getId() ).dispatch()
// Respond immediately — do not wait for jobs
messagebox.success( "Registration successful! Check your email." )
relocate( "dashboard" )
}
Chaining Jobs
// Job A dispatches Job B upon completion
class extends="cbq.models.AbstractJob" {
property name="generateReportJob" inject="GenerateReportJob";
function handle() {
// ... do work ...
// Dispatch next job in the chain
generateReportJob
.setReportId( reportId )
.delay( 5 )
.dispatch()
}
}
Idempotent Job
class extends="cbq.models.AbstractJob" {
property name="cache" inject="cachebox:default";
function handle() {
var lockKey = "job_processed_#orderId#"
if ( cache.get( lockKey ) ) {
return // Already processed — skip
}
// Process order...
processOrder( orderId )
// Mark as done (TTL: 1 hour)
cache.set( lockKey, true, 60 )
}
}
Best Practices
- Keep jobs small and focused — one job should do one thing
- Make jobs idempotent — safe to run multiple times in case of retries
- Never trust job payload without validation — validate properties in
handle()before use - Use the
failed()hook to log failures and optionally alert via monitoring - Set appropriate
timeoutper job type — short for emails, longer for PDF generation - Route by priority — use
highandlownamed queues with different worker counts - Avoid large payloads — store IDs in the job, not entire entity serializations
- Test job logic in unit tests — call
handle()directly without dispatching