Scheduling Tasks in Liquidsoap
Liquidsoap includes a lightweight scheduler that lets you run code at specific times or on a recurring basis. This is useful for automating things like playing a file every hour, announcing the top of the hour, or triggering a command at a specific time.
Scheduling in Liquidsoap works through threads — simple tasks that run in the background without interrupting your media streams. These threads are managed by the scheduler and executed as needed. They’re not operating system threads, just scheduled functions.
There are four main APIs available for scheduling:
thread.run– run a task on a regular intervalthread.when– run a task when a time-based condition becomes truecron.add/cron.remove– schedule tasks using familiar cron syntaxthread.run.recurrent– an advanced interface for custom scheduling
This page walks you through the first three, with examples to get you started.
If you are looking for a more in-depth example of how to use the scheduler, you can refer to our blog post Precise scheduling of tracks
thread.run: Simple Repeating Tasks
Use thread.run when you want to run a task repeatedly every N seconds. This is the most straightforward scheduling method.
Example: Log a message every 10 minutes
thread.run(
every=600.,
# This is the same as: fun () -> log("10 minutes have passed.")
{
log(
"10 minutes have passed."
)
}
)
This task will run every 600 seconds (10 minutes), logging a message.
You can schedule any function here — such as sending metadata, modifying a source, or queueing a track.
Example: Play a file every hour
thread.run(
every=3600.,
{request_queue.push(request.create("/path/to/hourly-jingle.mp3"))}
)
In this case, we assume that request_queue is a request.queue source used elsewhere in your script.
thread.when: Run at a Specific Time
To schedule a task at a specific time, use thread.when. It takes a time predicate — a Liquidsoap-specific language construct that returns true when the current time matches the given interval or time.
Example: Run a task at 9:00 AM
thread.when(
{9h},
{
log(
"It's 9 AM!"
)
}
)
This function is ran every time the predicates returns true, which should be during the 9th hour of the morning (hours are in 24h format).
You can refer to the thread.when and predicate.activates documentation for more details about the implementation.
Example: Queue a track at midnight
thread.when(
{23h59m},
{request_queue.push(request.create("/path/to/midnight-track.mp3"))}
)
cron.add and cron.remove: Cron-style Scheduling
If you’re used to cron syntax, you can use cron.add to schedule tasks using a familiar string format.
cron.add(
"0 12 * * *",
{
log(
"It’s noon!"
)
}
)
This example runs the task every day at 12:00 PM.
If needed, the function returns a unique identifier for the task, which you can use to remove it later:
let {id} =
cron.add(
"0 12 * * *",
{
log(
"It’s noon!"
)
}
)
Explicit IDs
You can also pass an explicit ID:
cron.add(
id="minight-task",
"0 0 * * *",
{
log(
"Midnight event"
)
}
)
If the ID is already registered, Liquidsoap will raise an error. This is useful for keeping track of scheduled tasks in complex scripts using meaningful IDs.
Removing a Cron Task
To remove a task, use cron.remove with its ID:
cron.remove("midnight-task")
Cron Syntax Recap
Cron strings follow the standard format:
minute hour day-of-month month day-of-week
Examples:
"0 0 * * *"– every day at midnight"*/5 * * * *"– every 5 minutes"15 14 * * 1-5"– weekdays at 2:15 PM
The implementation also supports the following shorthands: @annually, @yearly, @daily, @hourly, @monthly and @weekly.
Sharing state between tasks
Tasks run at the same time, on several cores, and alongside the stream itself. A reference is safe to share between them: every single read or write is indivisible. What is not indivisible is a group of them, and that is where a script that worked on one core starts to misbehave.
Take a "now playing" line kept in two references. A metadata callback writes the title, then the artist. A task that reads both can run between those two writes and print the new title with the previous artist.
The simplest fix is to not have two references: keep one holding a record, so the update and the read are each a single operation.
music = playlist("~/Music")
now_playing = ref({title = "", artist = ""})
def remember(m) =
now_playing := {title = m["title"], artist = m["artist"]}
end
music.on_metadata(synchronous=true, remember)
thread.run(
every=60.,
{
print(
"#{now_playing().title} - #{now_playing().artist}"
)
}
)
output.dummy(music)
When the values do have to live apart, group the writes with atomic, and group the reads the same way. atomic only holds back other atomic sections: a reader that does not use it still sees the writes one at a time.
music = playlist("~/Music")
current_title = ref("")
current_artist = ref("")
def remember(m) =
# Both writes land before any other atomic section runs.
atomic(
{
current_title := m["title"]
current_artist := m["artist"]
}
)
end
music.on_metadata(synchronous=true, remember)
# The two reads are grouped too. Read one at a time outside a section, and
# a title may be paired with the previous track's artist.
def now_playing() =
atomic(
{
"#{current_title()} - #{current_artist()}"
}
)
end
thread.run(every=60., {print(now_playing())})
output.dummy(music)
The other common case is something that must happen once, say connecting to a service the first time any task needs it. Reading a flag and then setting it is two operations, so two tasks can both find it unset. exchange sets a reference and returns what it replaced as one step. If the others must find the work done rather than merely claimed, put the work itself inside the section.
def connect() =
()
end
connected = ref(false)
# `exchange` writes the flag and hands back what it replaced in one step, so
# only the first caller sees `false`.
def connect_once() =
if not connected.exchange(true) then connect() end
end
# When the others must find the work finished, not just claimed, keep the
# work inside the section.
ready = ref(false)
def prepare_once() =
atomic(
{
if
not ready()
then
connect()
ready := true
end
}
)
end
thread.run(delay=0., connect_once)
thread.run(delay=0., prepare_once)
A few rules keep sections cheap and safe:
- A section is indivisible, not transactional. If the function raises, the changes it already made stand.
- Keep sections short and never wait on another task inside one: the task you wait for may be waiting to enter a section.
- Do not call source methods inside a section.
atomic(fast=true, f)makes waiting tasks spin instead of sleeping. Use it for a section that only touches a few references. It is wasteful for anything longer.
Advanced: thread.run.recurrent
For more complex scheduling needs, advanced users may use thread.run.recurrent. It allows full control over how a task is rescheduled after each execution, making it possible to implement dynamic or irregular schedules.
Most users won’t need this API, but it’s available if thread.run or cron.add don’t fit your needs.