# `PhoenixKit.ObanCrons`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.64.1/lib/phoenix_kit/oban_crons.ex#L1)

The Oban cron jobs installed modules declare, so a host does not have to copy them into
its config by hand.

A module declares its scheduled workers with the optional `c:PhoenixKit.Module.oban_crons/0`
callback, next to `c:PhoenixKit.Module.oban_queues/0`:

    @impl PhoenixKit.Module
    def oban_crons, do: [{"0 3 * * *", MyModule.Workers.NightlyRebuild}]

`mix phoenix_kit.install` writes every declared entry into the new crontab and
`mix phoenix_kit.update` adds the ones an existing crontab is missing
(`PhoenixKit.Install.ObanConfig.ensure_module_cron_entries/3`). The host's say wins, as
it does for core's own workers: a schedule the host wrote is never changed, and an entry
the host commented out of its crontab is a declined entry that is not offered again.

Entries are read from **every installed module**, enabled or not, and a module is found
without the registry when the installer runs before it exists (the same rule as
`PhoenixKit.ObanQueues`). A worker should do nothing while its module is switched off.

## One entry per worker

The installer decides whether an entry is already there by the worker's name, so a worker
has one schedule and no `args`. If two modules declare the same worker with different
schedules, the first (modules sorted by name) wins and the other is reported as a
conflict.

## What is dropped

An entry is ignored, with a warning, when the cron expression is not one Oban's own
parser accepts (`Oban.Cron.parse/1`, or `Oban.Plugins.Cron.parse/1` before Oban 2.24
renamed the plugin) or the worker is not a loaded Elixir module exporting `perform/1`.
That is deliberate:
Oban refuses to start when its crontab names something that is not a worker, so one bad
declaration must not reach the host's config.

# `conflict`

```elixir
@type conflict() :: %{
  worker: module(),
  kept: entry(),
  kept_owner: module(),
  ignored: entry(),
  ignored_owner: module()
}
```

Two modules declaring one worker with different schedules.

# `entry`

```elixir
@type entry() :: {String.t(), module()}
```

One cron entry: the schedule and the worker.

# `declared`

```elixir
@spec declared([module()] | nil) :: {[entry()], [conflict()]}
```

The cron entries to configure, one per worker, and the conflicts found on the way.

`modules` defaults to the registered modules when the registry is running and to a
`.beam` scan otherwise (the install and update tasks run before the registry exists).

# `describe_conflict`

```elixir
@spec describe_conflict(conflict()) :: String.t()
```

A one-line description of a conflict, for the installer's output.

---

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