# `Runic.Workflow.SchedulerPolicy`
[🔗](https://github.com/zblanco/runic/blob/main/lib/workflow/scheduler_policy.ex#L1)

Defines per-node scheduling policies for workflow execution.

A `SchedulerPolicy` controls retry behavior, timeouts, failure handling,
execution mode, and other scheduling concerns for individual workflow nodes.

Policies are resolved at runtime by matching against runnable nodes using
a list of `{matcher, policy_map}` tuples. The first matching rule wins,
and its policy map is merged over the default policy.

## Matcher Types

- `atom()` — exact match on the node's name
- `:default` — catch-all, always matches
- `{:name, %Regex{}}` — regex match on the node's name
- `{:type, module}` — match on the node's struct module
- `{:type, [modules]}` — match on any of the listed struct modules
- `fn/1` — custom predicate function receiving the node

## Backoff Strategies

- `:none` — no delay between retries
- `:linear` — `min(base_delay_ms * (attempt + 1), max_delay_ms)`
- `:exponential` — `min(base_delay_ms * 2^attempt, max_delay_ms)`
- `:jitter` — randomized exponential: `min(rand(base_delay_ms * 2^attempt), max_delay_ms)`

## Execution Modes

- `:sync` — standard synchronous execution (default)
- `:async` — asynchronous execution within the workflow's react cycle
- `:durable` — enables event emission (`%RunnableDispatched{}`, `%RunnableCompleted{}`,
  `%RunnableFailed{}`) for crash recovery and audit trails. Used by `Runic.Runner.Worker`
  to persist runnable lifecycle events in the workflow log.

## Fallback Functions

When all retries are exhausted and a `fallback` function is set, it receives
`(runnable, error)` and must return one of:

- `%Runnable{}` — a modified runnable to execute once (no further retries)
- `{:retry_with, %{key: value}}` — overrides merged into `meta_context`, then executed once
- `{:value, term}` — a synthetic value used as the step's output

Any other return value causes the runnable to fail with `{:invalid_fallback_return, value}`.

## Example

    alias Runic.Workflow.SchedulerPolicy

    policies = [
      {:call_llm, %{max_retries: 3, backoff: :exponential, timeout_ms: 30_000}},
      {{:type, Runic.Workflow.Step}, %{max_retries: 1, backoff: :linear}},
      {:default, %{timeout_ms: 10_000}}
    ]

    policy = SchedulerPolicy.resolve(runnable, policies)

# `fallback_fn`

```elixir
@type fallback_fn() ::
  (Runic.Workflow.Runnable.t(), term() -&gt; fallback_return()) | nil
```

# `fallback_return`

```elixir
@type fallback_return() ::
  Runic.Workflow.Runnable.t() | {:retry_with, map()} | {:value, term()}
```

# `t`

```elixir
@type t() :: %Runic.Workflow.SchedulerPolicy{
  backoff: :none | :linear | :exponential | :jitter,
  base_delay_ms: non_neg_integer(),
  circuit_breaker: map() | nil,
  deadline_ms: non_neg_integer() | nil,
  execution_mode: :sync | :async | :durable,
  executor: module() | :inline | nil,
  executor_opts: keyword(),
  fallback: fallback_fn(),
  idempotency_key: term() | nil,
  max_delay_ms: non_neg_integer(),
  max_retries: non_neg_integer(),
  on_failure: :halt | :skip,
  priority: :low | :normal | :high | :critical,
  timeout_ms: non_neg_integer() | :infinity
}
```

# `default_policy`

```elixir
@spec default_policy() :: t()
```

Returns the default policy struct.

# `fast_fail`

```elixir
@spec fast_fail() :: t()
```

Policy preset for fast-fail scenarios: no retries, 5s timeout, halt on failure.

# `io_policy`

```elixir
@spec io_policy(keyword()) :: t()
```

Policy preset for I/O-bound operations (HTTP, database, file system).

Defaults: 2 retries, linear backoff (500ms base), 10s timeout, skip on failure.
Override any default via `opts`.

# `llm_policy`

```elixir
@spec llm_policy(keyword()) :: t()
```

Policy preset for LLM / external AI model calls.

Defaults: 3 retries, exponential backoff (1s base, 30s max), 30s timeout, halt on failure.
Override any default via `opts`.

# `merge`

```elixir
@spec merge(map(), map()) :: map()
```

Merges two policy maps, with `overrides` taking precedence over `base`.

# `merge_policies`

```elixir
@spec merge_policies(list() | nil, list()) :: list()
```

Merges runtime override policies with workflow base policies.

In `:merge` mode (default), runtime overrides are prepended to the workflow base.
In `:replace` mode, only the runtime overrides are returned.

When `runtime_overrides` is `nil` or `[]`, returns `workflow_base` unchanged.

# `merge_policies`

```elixir
@spec merge_policies(list() | nil, list(), :merge | :replace) :: list()
```

# `new`

```elixir
@spec new(map() | keyword()) :: t()
```

Creates a new `SchedulerPolicy` from a map or keyword list.

Raises `ArgumentError` if any unknown keys are provided.

# `resolve`

```elixir
@spec resolve(Runic.Workflow.Runnable.t(), list() | nil) :: t()
```

Resolves a `SchedulerPolicy` for a given runnable by walking a list of
`{matcher, policy_map}` tuples top-to-bottom. First match wins.

The matched policy map is merged over the default policy. If no match is
found or `policies` is `nil` or `[]`, returns the default policy.

---

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