Scheduled Tasks
Human, Fluent, Functional Scheduled Tasks with BoxLang
🎯 Introduction
The BoxLang async framework provides a powerful and flexible way to schedule tasks and workloads in your applications. Whether you need to run tasks at specific intervals, one-off tasks, or manage complex scheduling scenarios, the async package has you covered. It allows you to schedule tasks using a human-readable DSL (Domain Specific Language) that is both fluent and functional. This makes it easy to define when and how tasks should run, without getting bogged down in complex configurations.
You have three main approaches to scheduling tasks in BoxLang:
📋 Scheduler Approach: Create a scheduler and register tasks in it
⚡ Scheduled Executor Approach: Create a
ScheduledExecutorand send task objects into it🖥️ CLI Runner Approach: Use the
boxlang schedule {path.to.Scheduler.bx}command to run tasks from the CLI
And with BoxLang 1.14.0, you can create ad-hoc schedulers on the fly using the new schedulerNew() BIF — no class file required:
// Create a named scheduler with a specific timezone
myScheduler = schedulerNew(
name = "email-scheduler",
timezone = "America/Chicago",
force = false
)
// Register tasks directly on the scheduler
myScheduler.task( "nightly-report" )
.call( () => generateReport() )
.everyDayAt( "02:30" )
myScheduler.startup()With our scheduled tasks you can run either one-off tasks or periodically tasks.
The way to do executor tasks is documented in our Executors Section, this guide focuses on the scheduler runtime and CLI runner approaches.
🏗️ Scheduler Service
BoxLang provides a SchedulerService that manages all the global, application, and module schedulers. There is really no need to interact with it, but if you want to you can access it via the boxRuntime().getSchedulerService() method. This service is responsible for managing all the schedulers in your application, including starting and stopping them, and providing access to the registered tasks.
⚙️ Runtime Configuration
The BoxLang configuration file located at {BoxLangHome}/config/boxlang.json contains all the necessary configurations and tunings for the scheduled tasks framework. Here are the main configurations you can set:
Executor
The executor property defines the default executor to use for all scheduled tasks. This can be overridden on a per-scheduler basis. The default executor is scheduled-tasks, which is a ScheduledExecutor with a default of 20 threads, which can be found in the executors section.
Cache Name
The cacheName property defines the cache that BoxLang's scheduler uses for server fixation — making sure a task that's registered identically on every node of a clustered/multi-server deployment only actually executes on one of those nodes per run, instead of firing redundantly on each one. The default cache is default, which is the default cache defined in the BoxLang configuration. This can be found in the caches section of the configuration file and can be overridden on a per-scheduler basis.
cacheName only fixates tasks across a cluster if it points at a real distributed cache (Redis, etc.) that is actually shared by every server. The bundled default cache is an in-memory store local to each JVM — pointing cacheName at it will not throw an error, but it only fixates tasks within that single JVM, not across physically separate servers, which is rarely what you want in a cluster.
See 🔒 Server Fixation (Clustering) below for how to enable it on a task and how the underlying lock behaves.
Schedulers
The schedulers property is an array of BoxLang schedulers to register upon startup. Each scheduler is defined by an absolute path to the scheduler class (e.g. /path/to/Scheduler.bx). You can use the ${user-dir} or ${boxlang-home} variables or any other environment variable to define the path. This allows you to define multiple schedulers that can be registered and managed by the SchedulerService at runtime startup.
TasksFile
The tasksFile property defines the file where HTTP-driven tasks are registered. This is typically managed by the bx:schedule component, which writes task definitions to this file and reloads them on startup. By default, this file is located at ${boxLangHome}/config/tasks.json. This file contains the definitions of tasks that are created and managed via the bx:schedule component, which allows you to create and manage scheduled tasks at runtime without needing to define them in a scheduler class.
See the bx:schedule component guide for the recommended way to create and manage these tasks at runtime.
bx:schedule Component
For HTTP-driven scheduled tasks, BoxLang provides the bx:schedule component (also available as cfschedule for CFML compatibility). This tag/script API lets you create, update, delete, pause, resume, and list scheduled tasks without writing a Scheduler class. Tasks created this way persist automatically to disk and survive runtime restarts.
See the full Schedule Component Guide and the bx:schedule reference for all available attributes and actions.
⏳ Schedulers
A Scheduler is a self-contained class that can track multiple scheduled tasks for you and give you enhanced and fluent approaches to scheduling. It is a powerful tool that allows you to register tasks, configure them, and manage their execution. Each scheduler class inherits dynamically from the BaseScheduler Java class, giving you access to all of its powerful methods and capabilities.
You can find the API Docs here: https://s3.amazonaws.com/apidocs.ortussolutions.com/boxlang/1.3.0/ortus/boxlang/runtime/async/tasks/BaseScheduler.html
Let's review the structure of a BoxLang scheduler class:
🕹️Properties
The scheduler properties are automatically injected by the BoxLang runtime and provide access to various services:
scheduler
The BaseScheduler instance that your class wraps, providing access to all scheduler methods
runtime
The BoxRuntime instance for access to global services
logger
A logger instance specifically configured for this scheduler
asyncService
The AsyncService for managing executors and async operations
cacheService
The CacheService for distributed scheduling and state management
interceptorService
The InterceptorService for broadcasting events and interceptors
🔧 Configuration Methods
Your scheduler class has access to all the methods from the BaseScheduler class through the scheduler property:
getRegisteredTasks()
Get a list of all registered task names
getTaskRecord( name )
Get the task record for a specific task
getTaskStats()
Get statistics for all tasks
hasTask( name )
Check if a task is registered
removeTask( name )
Remove a task from the scheduler
restart()
Restart the scheduler
restart( force, timeout )
Restart the scheduler with force flag and custom timeout
clearTasks()
Clear all tasks from the scheduler (usually done by restart)
startupTask( task )
Manually startup a specific task
startupTask( taskName )
Manually startup a task by name
hasStarted()
Check if the scheduler has been started
isRunning()
Alias for hasStarted() - check if scheduler is running
getStartedAt()
Get the timestamp when the scheduler was started
setContext( context )
Set the BoxLang context for task execution
getContext()
Get the current BoxLang context
setSchedulerName( name )
Set the human-readable name for this scheduler
setTimezone( timezone )
Set the timezone for all tasks (default: system timezone)
setDefaultTimezone()
Set the timezone to system default
getAsyncService()
Get the async service bound to this scheduler
setExecutor( executor )
Set the executor record for this scheduler
getExecutor()
Get the executor record
getLogger()
Get the logger instance for this scheduler
shutdown()
Shutdown the scheduler gracefully
shutdown( force )
Shutdown with force flag
shutdown( force, timeout )
Shutdown with force flag and timeout
startup()
Start the scheduler and all its tasks
task( name )
Register a new task with the given name
task( name, group )
Register a new task with name and group
xtask( name )
Register a new task but disable it immediately (useful for debugging)
xtask( name, group )
Register a disabled task with name and group
We always recommend you give a scheduler a name and a timezone.
🌍 Timezone
By default, all tasks will ask the scheduler for the timezone to run in, which most likely will be the runtime timezone. However, you can override it on a task-by-task basis using the setTimezone( timezone ) method:
You can find all valid time zone Id's here: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/ZoneId.html
Remember that some timezones utilize daylight savings time. When daylight saving time changes occur, your scheduled task may run twice or even not run at all. For this reason, we recommend avoiding timezone scheduling when possible.
🚀 Scheduling Tasks
Now that we have seen the capabilities of the scheduler, let's dive deep into scheduling tasks with the task( name ) method.
📝 Registering Tasks
Once you call this method, the scheduler will create a ScheduledTask object for you, configure it, and register it. The task object provides a fluent API for configuring when and how the task should run.
🎯 Task Closure/Lambda/Object
You register the callable event via the call() method on the task object. You can register a closure/lambda or an object. If you register an object, then we will call the object's run() method by default, but you can change it using the method argument and call any public method.
📛 Task Names and Groups
Always provide meaningful and unique task names as they serve as the primary identifier for your tasks. Task names are used for:
Logging and debugging - All log entries reference the task by name
Statistics and monitoring - Task metrics are tracked by name
Management operations - Starting, stopping, and retrieving tasks by name
Error reporting - Exception messages include the task name for identification
Groups provide additional organization benefits:
Logical organization - Group related tasks together (e.g., "maintenance", "reports", "monitoring")
Bulk operations - Manage multiple tasks as a group
Statistics aggregation - View metrics by task group
Easier maintenance - Quickly identify and manage task categories
⏰ Frequencies
There are many many frequency methods in scheduled tasks that will enable the tasks in specific intervals. Every time you see that an argument receives a timeUnit the available options are:
days
hours
minutes
seconds
milliseconds (default)
microseconds
nanoseconds
Ok, let's go over the frequency methods:
every( period, timeunit )
Run the task every custom period of execution
cron( expression )
Schedule the task using a cron expression (5-field Unix or 6-field Quartz)
spacedDelay( spacedDelay, timeunit )
Run the task every custom period of execution but with NO overlaps
everySecond()
Run the task every second from the time it gets scheduled
everyMinute()
Run the task every minute from the time it get's scheduled
everyHour()
Run the task every hour from the time it get's scheduled
everyHourAt( minutes )
Set the period to be hourly at a specific minute mark and 00 seconds
everyDay()
Run the task every day at midnight
everyDayAt( time )
Run the task daily with a specific time in 24 hour format: HH:mm
everyWeek()
Run the task every Sunday at midnight
everyWeekOn( day, time )
Run the task weekly on the given day of the week and time
everyMonth()
Run the task on the first day of every month at midnight
everyMonthOn( day, time )
Run the task every month on a specific day and time. If day is past the end of a shorter month (for example 31 in February), it runs on that month's last day (1.18+)
onFirstBusinessDayOfTheMonth( time )
Run the task on the first Monday of every month
onLastBusinessDayOfTheMonth( time )
Run the task on the last business day of the month
everyYear()
Run the task on the first day of the year at midnight
everyYearOn( month, day, time )
Set the period to be weekly at a specific time at a specific day of the week
onWeekends( time )
Run the task on Saturday and Sunday
onWeekdays( time )
Run the task only on weekdays at a specific time.
onMondays( time )
Only on Mondays
onTuesdays( time )
Only on Tuesdays
onWednesdays( time )
Only on Wednesdays
onThursdays( time )
Only on Thursdays
onFridays( time )
Only on Fridays
onSaturdays( time )
Only on Saturdays
onSundays( time )
Only on Sundays
All time arguments are defaulted to midnight (00:00)
🗓️ Cron Expression Scheduling
Use .cron( expression ) to schedule a task using a standard cron expression instead of the named frequency methods. Both 5-field Unix and 6-field Quartz formats are supported.
⏱️ Time Unit Methods
You can also use fluent time unit methods to set the time unit for your periods when using the every() method:
inDays()
Set the time unit to days
inHours()
Set the time unit to hours
inMinutes()
Set the time unit to minutes
inSeconds()
Set the time unit to seconds
inMilliseconds()
Set the time unit to milliseconds
inMicroseconds()
Set the time unit to microseconds
inNanoseconds()
Set the time unit to nanoseconds
Usage Examples:
🚫 Preventing Overlaps / Stacking
By default all tasks that have interval rates/periods that will execute on that interval schedule. However, what happens if a task takes longer to execute than the period? Well, by default the task will not execute if the previous one has not finished executing, causing the pending task to execute immediately after the current one completes ( Stacking Tasks ). If you want to prevent this behavior, then you can use the withNoOverlaps() method and BoxLang will register the tasks with a fixed delay. Meaning the intervals do not start counting until the last task has finished executing.
Spaced delays are a feature of the Scheduled Executors. There is even a spacedDelay( delay, timeUnit ) method in the Task object.
🔒 Server Fixation (Clustering)
If you deploy the same codebase — and therefore the same Scheduler.bx, with the same tasks — to every node of a clustered/multi-server environment, every node will, by default, register and run those tasks independently. A task scheduled with everyDayAt( "02:00" ) doesn't run once for the cluster; it runs once per node, at the same time, which is rarely what you want for things like nightly cleanup jobs, report generation, or cache warm-ups.
Server fixation solves this. Calling .onOneServer() on a task tells BoxLang to use the scheduler's cacheName cache as a distributed mutual-exclusion lock, so that no matter how many nodes have the task registered, only one node actually executes it on any given run.
How the lock behaves
When the scheduled time arrives, every node that has the task registered races to write a lock key into the
cacheNamecache. Whichever node's write lands first wins that run; every other node sees the key already taken and skips its own execution for that cycle.The lock is not actively released after a successful run — it's simply left to expire out of the cache on its own. This is deliberate: if the winning node crashed mid-run instead of completing normally, an actively-released-only lock would leave every other node permanently locked out with no run ever completing. Letting the lock expire naturally is what allows a surviving node to pick up the next scheduled run instead.
The lock's expiration is sized to the task's real-world recurrence, not to the short interval BoxLang's scheduler internally polls at to check whether a task is due to run.
Pointing at a real distributed cache
.onOneServer() only fixates across physically separate servers if cacheName points at a cache backend those servers actually share — a Redis-backed cache (via the bx-redis module) is a common choice:
With this configuration, all of the cluster's nodes can register the exact same scheduler unchanged — the fixation lock in clusterLocks (Redis) is what guarantees only one of them actually runs monthly-billing-report on any given firing, while a node's crash mid-run still lets a surviving node take over once the lock expires.
getStats() on a task includes inetHost and localIp — after a fixated run, these tell you which server actually won the lock and executed the task, which is handy for confirming fixation is working as expected.
⏳ Delaying First Execution
Every task can also have an initial delay of first execution by using the delay() method.
The delay is numeric and the timeUnit can be:
days
hours
minutes
seconds
milliseconds (default)
microseconds
nanoseconds
🎯 One Off Tasks
Apart from registering tasks that have specific intervals/frequencies you can also register tasks that can be executed ONCE ONLY. These are great for warming up caches, registering yourself with control planes, setting up initial data collections and so much more.
Basically, you don't register a frequency just the callable event. Usually, you can also combine them with a delay of execution, if you need them to fire off after certain amount of time has passed.
🔄 Life-Cycle Methods
We already saw that a scheduler has life-cycle methods, but a task can also have several useful life-cycle methods:
after( target )
Store the closure to execute after the task executes: function( task, results )
before( target )
Store the closure to execute before the task executes: function( task )
onFailure( target )
Store the closure to execute if there is a failure running the task: function( task, exception )
onSuccess( target )
Store the closure to execute if the task completes successfully: function( task, results )
✅ Truth Test Constraints
There are many ways to constrain the execution of a task. However, you can register a when() closure that will be executed at runtime and boolean evaluated. If true, then the task can run, else it is disabled.
📅 Start and End Dates
All scheduled tasks support the ability to seed in the startOnDateTime and endOnDateTime dates via our DSL:
startOn( date, time = "00:00" )endOn( date, time = "00:00" )
This means that you can tell the scheduler when the task will become active on a specific date and time (using the scheduler's timezone), and when the task will become disabled.
🕐 Start and End Times
All scheduled tasks support the ability to seed in the startTime and endTime dates via our DSL:
startOnTime( time = "00:00" )endOnTime( time = "00:00" )between( startTime = "00:00", endTime "00:00" )
This means that you can tell the scheduler to restrict the execution of the task after and/or before a certain time (using the scheduler's timezone).
⏸️ Disabling/Pausing Tasks
Every task is runnable from registration according to the frequency you set. However, you can manually disable a task using the disable() method:
Once you are ready to enable the task, you can use the enable() method:
Registering a task as disabled can lead to a task continuing to execute if it was later enabled and then removed via removeTask( name ) and not disabled again before doing so.
📊 Task Stats
All tasks keep track of themselves and have lovely metrics. You can use the getStats() method to get a a snapshot structure of the stats in time. Here is what you get in the stats structure:
created
The timestamp of when the task was created in memory
group
The name of the task group if any, this can be empty
inetHost
The hostname of the machine this task is registered with
lastRun
The last time the task ran, null by default
lastResult
The last result the task callable produced. This is an Attempt
lastExecutionTime
How long the last execution took in milliseconds
localIp
The ip address of the server this task is registered with
name
The name of the task
neverRun
A boolean flag indicating if the task has NEVER been ran
nextRun
When the task will run next, null by default
totalFailures
How many times the task has failed execution, 0 by default
totalRuns
How many times the task has run, 0 by default
totalSuccess
How many times the task has run and succeeded, 0 by default
🛠️ Task Helpers
We have created some useful methods that you can use when working with asynchronous tasks:
hasScheduler()
Verifies if the task is assigned a scheduler or not
isAnnually()
If the task is scheduled annually
isDisabled()
Verifies if the task has been disabled by bit
isEnabled()
Verifies if the task has been enabled (opposite of isDisabled())
isConstrained()
Verifies if the task has been constrained to run by dayOfMonth, dayOfWeek, firstBusinessDay, lastBusinessDay, weekdays, weekends, startOnDateTime, endOnDateTime, startTime, endTime
isScheduled()
Verifies if the task has been scheduled for execution
isNoOverlaps()
Verifies if the task has been configured to prevent overlapping executions
isOnOneServer()
Verifies if the task has been configured for server fixation
start()
This kicks off the task into the scheduled executor manually. This method is called for you by the scheduler upon application startup or module loading.
enable()
Enable the task for execution (sets disabled flag to false)
disable()
Disable the task from execution (sets disabled flag to true)
run( force )
Execute the task manually with optional force flag to bypass constraints
checkInterrupted()
Call periodically in long-running tasks to check if thread has been interrupted
cleanupTaskRun()
Internal cleanup method called after every task execution
getNow()
Get the current date/time in the task's configured timezone
getLastResult()
Get the last result of the task execution as an Optional
setMeta( struct )
Set the meta struct of the task. This is a placeholder for any data you want to be made available to you when working with a task
setMetaKey( key, value )
Set a key on the custom meta struct.
deleteMetaKey( key )
Delete a key from the custom meta struct.
getMeta()
Get the complete meta struct for the task
setTimezone( timezone )
Set the timezone for the task (accepts string or ZoneId)
getTimezone()
Get the timezone configured for the task
setName( name )
Set the human-readable name of the task
getName()
Get the human-readable name of the task
setGroup( group )
Set the group name for the task for logical organization
getGroup()
Get the group name for the task
getStats()
Get the statistics struct for the task containing execution metrics
🌐 Global Schedulers
The global schedulers are the default schedulers that are registered upon startup. These are defined in the schedulers property of the configuration file we have seen above. You can define multiple global schedulers that can be used throughout your application.
📱 Per-Application Schedulers
The per-application schedulers are the schedulers that are registered for a specific application using the Application.bx. Just use the this.schedulers property to define the schedulers you want to register for your application. This is useful if you want to have different schedulers for different applications in your BoxLang environment.
Please note that you do not need to register absolute paths for schedulers in your application, you can use relative paths or even per-app mappings. The SchedulerService will automatically resolve the paths for you. Once your application starts, the SchedulerService will register all the schedulers defined in the this.schedulers property. Once the application stops, the SchedulerService will automatically shutdown all the schedulers and their associated executors.
📁 Scheduler Logging
BoxLang provides dedicated logging for all scheduling operations through the scheduler.log file located in the logs folder of your BoxLang home directory. Please leverage logging as much as possible, as in async logging is critical for debugging and monitoring executor behavior.
Automatic Logging
All scheduler operations are automatically logged:
Scheduler creation and configuration
Task submissions and completions
Shutdown events and timing
Error conditions and exceptions
Performance warnings
Manual Logging
You can send custom messages to the scheduler log:
Available Log Types:
"Information"- General operational messages - (Default)"Warning"- Performance issues or concerns"Error"- Execution failures and exceptions"Debug"- Development and troubleshooting info"Trace"- Detailed execution flow information
Log File Location
Monitor this file for:
Scheduler performance issues
Task execution failures
Resource exhaustion warnings
Shutdown timing problems
Last updated
Was this helpful?
