# How to Implement Declarative and Dynamic Cron Scheduling in Jido

> Learn declarative and dynamic cron scheduling in Jido. Implement recurring work with pure directives and let Jido handle process management effectively.

- Repository: [agentjido/jido](https://github.com/agentjido/jido)
- Tags: how-to-guide
- Published: 2026-03-09

---

**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`](https://github.com/agentjido/jido/blob/main/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`](https://github.com/agentjido/jido/blob/main/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`](https://github.com/agentjido/jido/blob/main/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`](https://github.com/agentjido/jido/blob/main/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:

```elixir
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:

```elixir
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:

```elixir
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:

```elixir
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.