# `BullMQ.JobScheduler`
[🔗](https://github.com/taskforcesh/bullmq/blob/v2.2.3/lib/bullmq/job_scheduler.ex#L1)

Job scheduler for creating recurring jobs.

The JobScheduler allows you to create jobs that repeat on a schedule,
using either cron expressions or fixed intervals. This supersedes the
older "repeatable jobs" concept.

> #### Node.js Interoperability Note {: .warning}
>
> **Seconds field:** Node.js supports an optional 6th field for seconds at the
> beginning (`second minute hour day month weekday`). Elixir's `crontab` library
> uses standard 5-field format by default. **6-field expressions with seconds
> from Node.js will fail to parse in Elixir.**
>
> **Sunday:** Elixir uses `7` for Sunday, Node.js uses `0` (or `7`).
> Use `7` for Sunday to ensure compatibility.
>
> | Feature | Elixir | Node.js | Compatible? |
> |---------|--------|---------|-------------|
> | 5-field (no seconds) | ✅ | ✅ | ✅ Yes |
> | 6-field (with seconds) | ❌ | ✅ | ❌ No |
> | Sunday = 7 | ✅ | ✅ | ✅ Yes |
> | Sunday = 0 | ❌ | ✅ | ❌ No |
>
> **For cross-platform compatibility:**
> - Use 5-field cron expressions (no seconds)
> - Use `7` for Sunday, not `0`
> - Or use interval-based schedulers (`:every`)

## Usage

    # Create a scheduler with cron pattern
    {:ok, job} = BullMQ.JobScheduler.upsert(conn, queue_name, "daily_report",
      %{pattern: "0 9 * * *"},  # Every day at 9 AM
      "report",
      %{type: "daily"},
      []
    )

    # Create an interval-based scheduler (RECOMMENDED for cross-platform)
    {:ok, job} = BullMQ.JobScheduler.upsert(conn, queue_name, "heartbeat",
      %{every: 60_000},  # Every minute
      "ping",
      %{},
      []
    )

## Repeat Options

  * `:pattern` - Cron expression (e.g., "*/5 * * * *" for every 5 minutes)
  * `:every` - Interval in milliseconds (mutually exclusive with `:pattern`)
  * `:limit` - Maximum number of times to repeat
  * `:start_date` - When to start the schedule (milliseconds or DateTime)
  * `:end_date` - When to stop the schedule (milliseconds or DateTime)
  * `:tz` - Timezone for cron expressions (default: UTC)
  * `:immediately` - Run the first job immediately (only with pattern)
  * `:offset` - Offset in milliseconds for every-based jobs

## Cron Expressions

Cron expressions follow the standard 5-field format:

    ┌───────────── minute (0 - 59)
    │ ┌───────────── hour (0 - 23)
    │ │ ┌───────────── day of month (1 - 31)
    │ │ │ ┌───────────── month (1 - 12)
    │ │ │ │ ┌───────────── day of week (1 - 7) (Monday to Sunday)
    │ │ │ │ │
    * * * * *

> #### Weekday Numbering {: .info}
>
> | Day       | Elixir | Node.js | Compatible? |
> |-----------|--------|---------|-------------|
> | Monday    | 1      | 1       | ✅ Yes |
> | Tuesday   | 2      | 2       | ✅ Yes |
> | Wednesday | 3      | 3       | ✅ Yes |
> | Thursday  | 4      | 4       | ✅ Yes |
> | Friday    | 5      | 5       | ✅ Yes |
> | Saturday  | 6      | 6       | ✅ Yes |
> | Sunday    | 7      | 0 or 7  | ⚠️ Use `7` |
>
> **Use `7` for Sunday** - it works in both Elixir and Node.js.
> Avoid `0` for Sunday as it fails to parse in Elixir.

Examples:
  * `"0 * * * *"` - Every hour
  * `"*/15 * * * *"` - Every 15 minutes
  * `"0 9 * * 1-5"` - Every weekday at 9 AM
  * `"0 0 1 * *"` - First day of every month at midnight
  * `"0 0 * * 7"` - Every Sunday at midnight (Elixir only)

# `job_opts`

```elixir
@type job_opts() :: %{
  optional(:priority) =&gt; non_neg_integer(),
  optional(:lifo) =&gt; boolean(),
  optional(:delay) =&gt; non_neg_integer(),
  optional(:attempts) =&gt; pos_integer(),
  optional(:backoff) =&gt; map(),
  optional(:remove_on_complete) =&gt; boolean() | map(),
  optional(:remove_on_fail) =&gt; boolean() | map()
}
```

# `repeat_opts`

```elixir
@type repeat_opts() :: %{
  optional(:pattern) =&gt; String.t(),
  optional(:every) =&gt; non_neg_integer(),
  optional(:limit) =&gt; pos_integer(),
  optional(:start_date) =&gt; DateTime.t() | non_neg_integer(),
  optional(:end_date) =&gt; DateTime.t() | non_neg_integer(),
  optional(:tz) =&gt; String.t(),
  optional(:immediately) =&gt; boolean(),
  optional(:offset) =&gt; non_neg_integer(),
  optional(:count) =&gt; non_neg_integer()
}
```

# `scheduler_json`

```elixir
@type scheduler_json() :: %{
  :key =&gt; String.t(),
  :name =&gt; String.t(),
  optional(:next) =&gt; non_neg_integer(),
  optional(:iteration_count) =&gt; non_neg_integer(),
  optional(:limit) =&gt; non_neg_integer(),
  optional(:start_date) =&gt; non_neg_integer(),
  optional(:end_date) =&gt; non_neg_integer(),
  optional(:tz) =&gt; String.t(),
  optional(:pattern) =&gt; String.t(),
  optional(:every) =&gt; non_neg_integer(),
  optional(:offset) =&gt; non_neg_integer(),
  optional(:template) =&gt; map()
}
```

# `calculate_next_millis`

```elixir
@spec calculate_next_millis(repeat_opts(), non_neg_integer()) ::
  non_neg_integer() | nil
```

Calculates the next execution time for a repeat configuration.

## Parameters

  * `repeat_opts` - Repeat options with pattern or every
  * `reference_time` - Reference time in milliseconds (default: now)

## Returns

  * The next execution time in milliseconds, or nil if no next time

## Examples

    # Next minute
    next = BullMQ.JobScheduler.calculate_next_millis(%{every: 60_000}, now)

    # Next cron execution
    next = BullMQ.JobScheduler.calculate_next_millis(%{pattern: "0 * * * *"}, now)

# `count`

```elixir
@spec count(pid() | atom(), String.t(), keyword()) ::
  {:ok, non_neg_integer()} | {:error, term()}
```

Gets the count of job schedulers for a queue.

## Examples

    {:ok, count} = BullMQ.JobScheduler.count(conn, "my_queue")
    # => {:ok, 5}

# `get`

```elixir
@spec get(pid() | atom(), String.t(), String.t(), keyword()) ::
  {:ok, scheduler_json() | nil} | {:error, term()}
```

Gets a job scheduler by ID.

## Parameters

  * `conn` - Redis connection
  * `queue_name` - Queue name
  * `scheduler_id` - The scheduler ID
  * `opts` - Options (prefix)

## Returns

  * `{:ok, scheduler}` - The scheduler data
  * `{:ok, nil}` - Scheduler not found
  * `{:error, reason}` - Redis error

## Examples

    {:ok, scheduler} = BullMQ.JobScheduler.get(conn, "my_queue", "daily_report")
    # => %{
    #   key: "daily_report",
    #   name: "daily_report",
    #   pattern: "0 9 * * *",
    #   next: 1699999999000,
    #   iteration_count: 5,
    #   template: %{data: %{}, opts: %{}}
    # }

# `list`

```elixir
@spec list(pid() | atom(), String.t(), keyword()) ::
  {:ok, [scheduler_json()]} | {:error, term()}
```

Lists all job schedulers for a queue.

## Parameters

  * `conn` - Redis connection
  * `queue_name` - Queue name
  * `opts` - Options:
    * `:start` - Start index (default: 0)
    * `:end` - End index (default: -1 for all)
    * `:asc` - Sort ascending (default: false)
    * `:prefix` - Key prefix (default: "bull")

## Returns

  * `{:ok, schedulers}` - List of scheduler data
  * `{:error, reason}` - Redis error

## Examples

    {:ok, schedulers} = BullMQ.JobScheduler.list(conn, "my_queue")
    {:ok, first_10} = BullMQ.JobScheduler.list(conn, "my_queue", start: 0, end: 9)

# `remove`

```elixir
@spec remove(pid() | atom(), String.t(), String.t(), keyword()) ::
  {:ok, boolean()} | {:error, term()}
```

Removes a job scheduler and its next scheduled job.

## Parameters

  * `conn` - Redis connection
  * `queue_name` - Queue name
  * `scheduler_id` - The scheduler ID to remove
  * `opts` - Options (prefix)

## Returns

  * `{:ok, true}` - Scheduler was removed
  * `{:ok, false}` - Scheduler not found
  * `{:error, reason}` - Redis error

## Examples

    {:ok, true} = BullMQ.JobScheduler.remove(conn, "my_queue", "daily_report")

# `remove_by_key`

```elixir
@spec remove_by_key(pid() | atom(), String.t(), String.t(), keyword()) ::
  {:ok, boolean()} | {:error, term()}
```

Removes a job scheduler by its key (alias for remove/4).

For backwards compatibility with the "repeatable jobs" API.

# `upsert`

```elixir
@spec upsert(
  pid() | atom(),
  String.t(),
  String.t(),
  repeat_opts(),
  String.t(),
  map(),
  keyword()
) :: {:ok, BullMQ.Job.t()} | {:error, atom()}
```

Creates or updates a job scheduler.

If a scheduler with the given ID already exists, it will be updated.
The scheduler will create a delayed job for the next scheduled execution.

## Parameters

  * `conn` - Redis connection
  * `queue_name` - Queue name
  * `scheduler_id` - Unique identifier for the scheduler
  * `repeat_opts` - Repeat configuration (pattern, every, limit, etc.)
  * `job_name` - Name for the jobs created by this scheduler
  * `job_data` - Data to be passed to each job
  * `opts` - Job options (priority, attempts, backoff, etc.)

## Returns

  * `{:ok, job}` - The job struct for the next scheduled execution
  * `{:error, :both_pattern_and_every}` - Both pattern and every specified
  * `{:error, :no_pattern_or_every}` - Neither pattern nor every specified
  * `{:error, :immediately_with_start_date}` - Both immediately and start_date specified
  * `{:error, :limit_reached}` - Job has reached its iteration limit
  * `{:error, :end_date_reached}` - Job has passed its end date
  * `{:error, :job_id_collision}` - A job with the same ID already exists in a non-updatable state
  * `{:error, :job_slots_busy}` - Both current and next time slots have jobs

## Examples

    # Every day at 9 AM
    {:ok, job} = BullMQ.JobScheduler.upsert(conn, "emails", "daily_digest",
      %{pattern: "0 9 * * *"},
      "send_digest",
      %{recipient: "all"},
      priority: 10
    )

    # Every 5 minutes, limited to 100 executions
    {:ok, job} = BullMQ.JobScheduler.upsert(conn, "health", "heartbeat",
      %{every: 300_000, limit: 100},
      "ping",
      %{timestamp: true},
      []
    )

    # Start immediately then repeat every hour
    {:ok, job} = BullMQ.JobScheduler.upsert(conn, "sync", "data_sync",
      %{pattern: "0 * * * *", immediately: true},
      "sync_data",
      %{},
      []
    )

---

*Consult [api-reference.md](api-reference.md) for complete listing*
