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 in lib/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 in lib/jido/agent/directive/cron_cancel.ex): A directive for removing previously registered jobs by their logical ID.
  • Jido.AgentServer.Signal.CronTick (defined in lib/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 in lib/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:

  1. Construct a %Jido.Agent.Directive.Cron{} struct with your schedule and payload
  2. Return it from cmd/2 as part of the directives list
  3. The runtime automatically calls exec/3, which registers the job with Jido.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 cron field based on runtime values
  • Optionally specify a timezone for 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 from cmd/2 to 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 from state.cron_jobs.
  • Direct access to Jido.Scheduler is 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →