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:

  1. 📋 Scheduler Approach: Create a scheduler and register tasks in it

  2. ⚡ Scheduled Executor Approach: Create a ScheduledExecutor and send task objects into it

  3. 🖥️ CLI Runner Approach: Use the boxlang schedule {path.to.Scheduler.bx} command to run tasks from the CLI

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:

// BoxLang Scheduler
// These are managed by the SchedulerService and registered upon startup
// or via a boxlang schedule [scheduler.bx] call
"scheduler": {
    // The default scheduler for all scheduled tasks
    // Each scheduler can have a different executor if needed
    "executor": "scheduled-tasks",
    // The cache to leverage for server fixation or distribution
    "cacheName": "default",
    // An array of BoxLang Schedulers to register upon startup
    // Must be an absolute path to the scheduler file
    // You can use the ${user-dir} or ${boxlang-home} variables or any other environment variable
    // Example: "schedulers": [ "/path/to/Scheduler.bx" ]
    "schedulers": [],
    // You can also define tasks manually here
    // Every task is an object defined by a unique name
    // The task object is a struct with the following properties:
    // - `crontime:string` - The cron time to run the task (optional), defaults to empty string
    // - `eventhandler:path` - The absolute path to the task event handler(optional), defaults to empty string
    // - `exclude:any` - Comma-separated list of dates or date range (d1 to d2) on which to not execute the scheduled task
    // - `file:name` - Name of the log file to store output of the task (optional), defaults to `scheduler`
    // - `group:string` - The group name of the task (optional), defaults to empty string
    "tasks": {}
},

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 to use for server fixation or distribution. This is useful if you want to share scheduled tasks across multiple servers in a cluster. 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.

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.

Tasks

The tasks property is an object that defines the tasks to register upon startup. Each task is defined by a unique name and can have many properties. This is an experimental feature that is coming soon.

⏳ 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:

Property
Description

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:

Method
Description

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

🌍 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:

🚀 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:

Frequency Method
Description

every( period, timeunit )

Run the task every custom period of execution

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

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

⏱️ Time Unit Methods

You can also use fluent time unit methods to set the time unit for your periods when using the every() method:

Time Unit Method
Description

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.

⏳ 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

Please note that the delay pushes the execution of the task into the future only for the first execution.

🎯 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:

Method
Description

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:

📊 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:

Metric
Description

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:

Method
Description

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

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.

💻 CLI Runner

You can also run schedulers from the command line using the BoxLang CLI. This is useful for running scheduled tasks in CI/CD pipelines, containerized environments, system cron jobs, or for testing purposes. The CLI runner provides a standalone way to execute scheduler files directly from the operating system level.

Command Syntax

Requirements

  • File Extension: Must be a .bx (BoxLang) file

  • File Content: Should contain a BoxLang class with scheduler definitions

  • File Path: Can be absolute or relative to current directory

Lifecycle

When you run a scheduler via the CLI, BoxLang follows this lifecycle:

  1. 🔍 File Validation: The file is checked for existence and proper .bx extension

  2. ⚙️ Compilation: File is compiled and validated for syntax errors

  3. 🏗️ Instantiation: Scheduler class is instantiated with injected dependencies

  4. 📋 Registration: Scheduler is registered with the BoxLang SchedulerService

  5. 🚀 Startup: All configured tasks begin execution according to their schedules

  6. ⏳ Blocking: Process runs continuously until manually stopped

  7. 🛑 Graceful Shutdown: Press Ctrl+C to gracefully shutdown all tasks

Examples

CLI Help

You can get detailed help for the schedule command:

This displays comprehensive usage information, requirements, and examples.

Integration with System Services

The CLI runner is perfect for integration with system-level schedulers and process managers:

Systemd Service (Linux)

Docker Container

Cron Job (OS Level)

Environment Variables

You can use BoxLang environment variables with the CLI runner:

Logging and Monitoring

The CLI runner integrates with BoxLang's logging system:

All log files will be stored in the BoxLang Home's logs directory under the scheduler.log file. You can monitor this file for real-time updates on task execution, errors, and performance metrics.

Process Management

When running via CLI, you can manage the process using standard OS tools:

Use Cases

The CLI runner is ideal for:

  • 🐳 Containerized Applications: Run schedulers in Docker/Kubernetes

  • ☁️ Cloud Functions: Execute scheduled tasks in serverless environments

  • 🔄 CI/CD Pipelines: Run maintenance tasks during deployments

  • 🖥️ System Administration: Replace traditional cron jobs with BoxLang schedulers

  • 🧪 Development & Testing: Quickly test scheduler configurations

  • 📊 Data Processing: Run ETL jobs and data synchronization tasks

  • 🔍 Monitoring: Execute health checks and system monitoring tasks

This will instantiate the scheduler, configure it, start it, and run it until it's manually stopped or all tasks complete (for one-off tasks).

📁 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

🎛️ Scheduler Management BIFs

BoxLang provides several Built-In Functions (BIFs) for managing schedulers at runtime. These functions allow you to interact with the scheduler service programmatically and manage schedulers dynamically.

schedulerStart()

Creates, registers, and starts a scheduler with the given instantiation class path.

Syntax:

Parameters:

  • className (required): The class name to instantiate (e.g., "models.myapp.MyScheduler")

  • name (optional): Override the scheduler name defined in the class

  • force (optional): Force start the scheduler (default: true)

Returns: The scheduler object

Example:

schedulerGet()

Get a specific scheduler by name from the scheduler service.

Syntax:

Parameters:

  • name (required): The name of the scheduler to retrieve

Returns: The scheduler object

Throws: IllegalArgumentException if scheduler not found

Example:

schedulerGetAll()

Get all registered schedulers as a struct.

Syntax:

Returns: A struct containing all registered schedulers (key = scheduler name, value = scheduler object)

Example:

schedulerList()

List all the scheduler names registered in the system.

Syntax:

Returns: An array of scheduler names

Example:

schedulerShutdown()

Shutdown a scheduler by name gracefully or forcefully.

Syntax:

Parameters:

  • name (required): The name of the scheduler to shutdown

  • force (optional): Force shutdown the scheduler (default: false)

  • timeout (optional): Timeout in seconds to wait for graceful shutdown (default: 30)

Example:

schedulerRestart()

Restart a scheduler by name (shutdown then startup).

Syntax:

Parameters:

  • name (required): The name of the scheduler to restart

  • force (optional): Force restart the scheduler (default: false)

  • timeout (optional): Timeout in seconds to wait for shutdown (default: 30)

Example:

schedulerStats()

Get statistics for all schedulers or a specific scheduler.

Syntax:

Parameters:

  • name (optional): The name of the scheduler to get stats for (if not provided, returns stats for all schedulers)

Returns: Stats struct(s) containing:

  • created: When the task was created

  • lastExecutionTime: Duration of last execution

  • lastResult: Result of last execution

  • lastRun: When the task last ran

  • name: Task name

  • neverRun: Boolean indicating if task has never run

  • nextRun: When the task will run next

  • totalFailures: Number of failed executions

  • totalRuns: Total number of executions

  • totalSuccess: Number of successful executions

Example:

💡 Example: Dynamic Scheduler Management

Here's a practical example of how you might use these BIFs to manage schedulers dynamically:

📄 Task Records

When tasks are registered in a scheduler, they are wrapped in a TaskRecord object that contains metadata about the task's lifecycle and execution state. You can access task records through the scheduler:

TaskRecord Properties

Property
Description

name

The task name

group

The task group

task

The actual ScheduledTask object

future

The ScheduledFuture object for the task

scheduledAt

When the task was scheduled

registeredAt

When the task was registered

disabled

Whether the task is disabled

error

Whether the task had an error during scheduling

errorMessage

The error message if any

stacktrace

The full stacktrace if any

inetHost

The hostname where the task is running

localIp

The IP address of the server

💎 Best Practices

Here are some best practices when working with BoxLang scheduled tasks:

🎯 Task Design

  • Keep tasks focused: Each task should have a single responsibility

  • Handle errors gracefully: Use onFailure() callbacks to handle exceptions

  • Use appropriate timing: Consider system load when scheduling frequent tasks

  • Leverage constraints: Use time and date constraints to avoid unnecessary executions

🔧 Configuration

  • Use groups: Organize related tasks into groups for better management

  • Set meaningful names: Use descriptive task names for easier debugging

  • Configure metadata: Store relevant information in task metadata

  • Choose appropriate executors: Match executor types to your workload patterns

📊 Monitoring

  • Track statistics: Use getStats() to monitor task performance

  • Implement logging: Use life-cycle callbacks for comprehensive logging

  • Monitor for failures: Set up alerts for task failures

  • Review execution times: Watch for tasks that run longer than expected

🚀 Performance

  • Avoid overlaps: Use withNoOverlaps() for long-running tasks

  • Optimize frequencies: Don't schedule tasks more frequently than necessary

  • Use virtual executors: For I/O-bound tasks, consider virtual thread executors

  • Clean up resources: Ensure tasks properly clean up any resources they use

🔒 Reliability

  • Handle timezone changes: Be aware of daylight saving time impacts

  • Plan for restarts: Design tasks to handle application restarts gracefully

  • Use constraints wisely: Combine multiple constraints to achieve desired scheduling

  • Test thoroughly: Test your schedulers in different scenarios and environments

This comprehensive guide covers all the essential aspects of BoxLang's scheduled tasks framework. Whether you're building simple cron-like jobs or complex distributed scheduling systems, BoxLang's scheduler provides the tools and flexibility you need.

Last updated

Was this helpful?