How to Implement Declarative and Dynamic Cron Scheduling in Jido
Jido enables declarative and dynamic cron scheduling through pure directives that describe recurring work, while the runtime handles process management via Jido.Scheduler and Jido.Agent.Directive.Cron.
Jido is an Elixir agent framework that separates the declaration of work from its execution. When implementing declarative and dynamic cron scheduling in Jido, you use immutable directives to describe when and what should run, allowing the framework to manage the underlying cron processes under the agent's supervision tree.
Understanding Jido's Cron Architecture
The cron system in Jido consists of four core components that work together to provide both declarative and dynamic scheduling capabilities:
Jido.Agent.Directive.Cron(defined inlib/jido/agent/directive/cron.ex): The declarative job description containing the cron expression, message payload, job ID, and optional timezone.Jido.Agent.Directive.CronCancel(defined inlib/jido/agent/directive/cron_cancel.ex): A directive for removing previously registered jobs by their logical ID.Jido.AgentServer.Signal.CronTick(defined inlib/jido/agent_server/signal/cron_tick.ex): The signal cast back to the agent on each tick, containing the job ID and message.Jido.Scheduler(defined inlib/jido/scheduler.ex): A thin wrapper around SchedEx that manages the actual cron processes, including starting, canceling, and inspecting jobs.
Declarative Cron Scheduling
Declarative scheduling in Jido follows a pure functional pattern where you describe the work without managing side effects. The agent returns a %Cron{} directive from its cmd/2 callback, and the runtime handles execution through the Jido.AgentServer.DirectiveExec protocol.
To implement a declarative cron job:
- Construct a
%Jido.Agent.Directive.Cron{}struct with your schedule and payload - Return it from
cmd/2as part of the directives list - The runtime automatically calls
exec/3, which registers the job withJido.Scheduler.run_every/4
Here is a complete example that sends a heartbeat signal every minute:
defmodule MyAgent do
use Jido.Agent
@impl true
def cmd(:init, _input, _state) do
cron = %Jido.Agent.Directive.Cron{
cron: "* * * * *", # every minute
job_id: :heartbeat, # logical identifier
message: %MyApp.Signals.Heartbeat{} # payload signal
}
{:ok, [], [cron]}
end
end
When this directive executes, Jido.Scheduler stores the process PID in the agent's state under state.cron_jobs[job_id], ensuring the job lifecycle is tied to the agent's supervision tree.
Dynamic Cron Scheduling
Dynamic scheduling allows you to compute cron expressions at runtime based on external input, configuration, or agent state. Because the %Cron{} directive is just a data structure, you can construct it dynamically within cmd/2 or helper functions.
To implement dynamic scheduling:
- Calculate the
cronfield based on runtime values - Optionally specify a
timezonefor locale-aware execution - Use the same directive return pattern as declarative scheduling
Here is an example that adjusts the interval based on user input:
def handle_input(%{type: "set_interval", interval: minutes}, state) do
cron_expr = "*/#{minutes} * * * *"
cron = %Jido.Agent.Directive.Cron{
cron: cron_expr,
job_id: :dynamic_job,
message: %MyApp.Signals.DoWork{},
timezone: "America/New_York"
}
{:ok, state, [cron]}
end
If you register a job with the same job_id twice, the new directive replaces the existing job. The runtime handles this by registering the new schedule with Jido.Scheduler.run_every/4, effectively updating the cron timing without requiring an explicit cancel directive.
Managing Cron Jobs
Jido provides the Jido.Agent.Directive.CronCancel directive for removing scheduled jobs. When returned from cmd/2, the runtime executes exec/3 on the directive, which looks up the PID in state.cron_jobs and calls Jido.Scheduler.cancel/1.
To cancel a job:
def stop_heartbeat(state) do
cancel = %Jido.Agent.Directive.CronCancel{job_id: :heartbeat}
{:ok, state, [cancel]}
end
This ensures proper cleanup of the underlying SchedEx process and removal of the job from the agent's state.
Advanced Direct Scheduler Access
For scenarios requiring direct process management outside the directive flow, you can interact with Jido.Scheduler directly. This is useful for one-off functions or when you need manual PID management.
The scheduler provides run_every/3 and run_every/5 functions that accept a function, cron expression, and options:
def start_custom_job do
fun = fn -> IO.puts("custom tick #{DateTime.utc_now()}") end
{:ok, pid} = Jido.Scheduler.run_every(fun, "*/2 * * * *", timezone: "Etc/UTC")
Process.send_after(self(), {:cancel, pid}, :timer.minutes(10))
end
Jobs started this way are not automatically tracked in the agent's cron_jobs state or cleaned up when the agent terminates, requiring manual management for cancellation and supervision.
Summary
- Declarative scheduling uses
%Jido.Agent.Directive.Cron{}structs returned fromcmd/2to describe recurring work without managing processes. - Dynamic scheduling computes cron expressions and timezones at runtime, allowing flexible schedule adjustments based on input or state.
- The runtime handles execution via
Jido.Scheduler, which wraps SchedEx and manages process lifecycles under the agent's supervision tree. - Cancellation uses
%Jido.Agent.Directive.CronCancel{}to cleanly stop jobs and remove them fromstate.cron_jobs. - Direct access to
Jido.Scheduleris available for advanced use cases requiring manual process management.
Frequently Asked Questions
What is the difference between declarative and dynamic cron scheduling in Jido?
Declarative scheduling involves returning a static %Cron{} directive from your agent's cmd/2 callback, describing exactly when and what should run. Dynamic scheduling calculates the cron expression, timezone, or message payload at runtime based on external input, configuration, or agent state, allowing schedules to change in response to runtime conditions without restarting the agent.
How does Jido handle timezone support for cron jobs?
Jido supports timezone-aware scheduling through the optional timezone field in the %Cron{} directive. When specified (e.g., "America/New_York"), the underlying Jido.Scheduler passes this option to SchedEx, ensuring the cron expression is evaluated against the specified locale's local time rather than UTC. If no timezone is provided, the scheduler defaults to UTC.
What happens if I register a cron job with the same job_id twice?
If you return a %Cron{} directive with a job_id that already exists in the agent's state.cron_jobs, the new directive replaces the existing job. The runtime handles this by registering the new schedule with Jido.Scheduler.run_every/4, effectively updating the cron timing without requiring an explicit %CronCancel{} directive. The old job is automatically cancelled and cleaned up.
Can I use Jido's scheduler without the agent directive system?
Yes, you can interact directly with Jido.Scheduler using functions like run_every/3 or run_every/5 to start cron processes outside the directive flow. However, jobs started this way are not automatically tracked in the agent's cron_jobs state or cleaned up when the agent terminates. You must manually manage the returned PID for cancellation and ensure proper supervision, making the directive-based approach preferable for most agent workflows.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →