# Scheduled jobs

> How to create and manage durable scheduled jobs in Fermix for digests, watchers, reminders, and periodic checks.

A scheduled job is a saved instruction that tells the agent to do something on a schedule, for example a daily digest, a periodic check, or a one-off reminder. Each run is bounded (it has time and step limits) and isolated (it starts fresh and cannot see your other chats). It gets its own session and memory scope, executes through the same `AgentLoop` (the agent's run-tools-and-respond cycle) used for interactive turns, and delivers its final response through a configured channel target. Jobs survive daemon restarts (the daemon is the long-running Fermix background process) and are stored in SQLite (a single-file local database).

## Schedule kinds

| Kind | Expression | Notes |
|------|------------|-------|
| Interval | `every N minutes`, `every N hours`, `every N days` | Examples: `every 5 minutes`, `every 6 hours` |
| Cron | 5-field cron string | Example: `0 9 * * *`. Supports `*`, lists (`1,15`), ranges (`9-17`), and steps (`*/15`). Weekday `7` and `0` both mean Sunday. |
| Once | ISO-8601 UTC datetime | Example: `2026-12-25T09:00:00Z`. Job disables itself after the single run. |

Free-form natural language like "daily at 8am" is rejected. Use the cron form `0 8 * * *` instead.

Each job stores a `timezone` (IANA label, default `UTC`). Cron expressions are evaluated in that timezone, including DST transitions. Unknown timezone strings are rejected at creation.

## Creating a job

Use the `schedule_job` tool in any conversation. The agent does not execute the task immediately; it only creates the job record.

Required parameters:

| Parameter | Type | Description |
|-----------|------|-------------|
| `name` | string | Human-readable label |
| `schedule` | string | Schedule expression (interval, cron, or ISO-8601 datetime) |
| `task` | string | Work instructions for the future run. Bake in any values the run will need (location, account, etc.) because scheduled runs are isolated and cannot see the originating conversation. |

Optional parameters:

| Parameter | Type | Description |
|-----------|------|-------------|
| `description` | string | Short note for the source catalog |
| `timezone` | string | IANA timezone for cron evaluation |
| `expires_at` | string | ISO-8601 UTC datetime after which the job is marked expired and stops running |
| `allowed_tools` | array of strings | Narrows the tool set available to the run. Must be a subset of tools visible to the caller; unknown names are rejected. |
| `skill_name` | string | Binds the run to an existing skill. The run executes inside that skill's confinement; `allowed_tools` and capability policy are intersected, never widened. Unknown skill names are rejected at creation. |
| `provider` | string | Pin the job's runs to a specific provider (`anthropic`, `openai`, `openai_codex`, `xai`, `openrouter`, `mistral`, or `ollama`; must be paired with `model`). The name is validated against Fermix's model catalog at creation time, not against your configured credentials — pinning a provider you have not set up creates the job, but its runs will fail until that provider is configured. |
| `model` | string | Provider-specific model id (must be paired with `provider`). Omit both to use the default cron route from `[fermix_core.routing]`. |
| `timeout_seconds` | integer | Wall-clock timeout per run (default: 1800, i.e. 30 minutes) |
| `inactivity_timeout_seconds` | integer | Triggers when the agent loop stops making progress (no provider or tool call) |
| `delivery_mode` | string | `none`, `origin`, `channel`, or `local` |
| `delivery_target` | object | Channel and chat id for delivery |

Example using `fermix ask` to create a daily digest job:

```bash
fermix ask 'Schedule a daily digest at 8am Eastern. \
  Task: fetch the top Hacker News stories and summarize them. \
  Deliver to my Telegram.'
```

The agent calls `schedule_job` with a cron expression `0 8 * * *`, timezone `America/New_York`, and a `delivery_mode` of `channel` resolved from the configured default.

## Default delivery configuration

Per-job delivery can be set at creation time, or inherited from a daemon-wide default in `config.toml`. The default is resolved into each job when it is created; changing the default later does not silently retarget existing jobs.

```toml
[fermix_core.jobs]
default_delivery_mode = "channel"

[fermix_core.jobs.default_delivery_target]
platform = "telegram"
chat_id = "8217352118"
```

`chat_id` is the external channel conversation id (for example, a Telegram numeric chat id), not a Fermix session id.

## Delivery modes

| Mode | Behavior |
|------|----------|
| `none` | No channel reply. Run output is still written to the run artifacts under `~/.fermix/job_runs/`. |
| `local` | No channel reply either — same delivery behavior as `none`. The run output stays in the run artifacts. |
| `origin` | Delivered to the channel and chat where the job was created. |
| `channel` | Delivered to an explicit `delivery_target` or the configured default target. |

Regardless of mode, every non-silent run also writes a summary to the job's memory source (see [Memory sources](#memory-sources) below).

If the agent's final response text matches the `silent_marker` (default `[SILENT]`), delivery is suppressed even when the mode would normally send.

A channel send that hits a transient connection problem (a request that never obtained a network connection, common right after the host wakes from sleep) is retried — up to three attempts with a short backoff — instead of being dropped. Only that no-connection case is retried; an error after the request went out fails fast, so a message is never sent twice.

## Job tools

| Tool | Description |
|------|-------------|
| `schedule_job` | Create a new scheduled job |
| `list_jobs` | List existing jobs. Each entry surfaces `task_prompt`, `skill_name`, `provider`, `model`, `delivery_mode`, and `delivery_target`. |
| `update_job` | Edit a job in place: task, schedule, description, `skill_name` rebinding, `provider`/`model` route pin (or `clear_route_pin: true` to un-pin both back to default routing — mutually exclusive with `provider`/`model`), or delivery (`delivery_mode`, `delivery_target`). Omitted fields are left unchanged. |
| `pause_job` | Pause a job without removing it |
| `resume_job` | Resume a paused job |
| `remove_job` | Delete a job and cascade-remove its run history |
| `run_job_now` | Trigger an immediate out-of-band run without disturbing the timed cadence. Refuses paused, disabled, expired, or already-running jobs. |
| `list_job_runs` | Read a job's execution history (newest first). Accepts an optional `status` filter. |
| `get_job_run` | Read one run in full: status, trigger, timing, token usage, prompt snapshot, final response, and error details. The `task_prompt` field reflects the instructions at run time, captured from the run's config snapshot. |
| `memory_sources_list` | List memory sources, including sources backed by scheduled jobs |

Full parameter documentation is on the [tool reference](/docs/tool-reference) page.

## Runtime bounds

Each run is bounded and isolated:

| Limit | Default | How to change |
|-------|---------|---------------|
| Wall-clock timeout | 30 minutes | Per job, via the `timeout_seconds` parameter |
| Delivery timeout | 60 seconds | Internal daemon default (not a `config.toml` key) |
| Agent-loop iterations | 100 | Internal daemon default (not a `config.toml` key) |
| Inactivity timeout | off unless set | Per job, via the `inactivity_timeout_seconds` parameter |

The daemon-wide defaults (delivery timeout, iteration cap, and the freshness window below) are built-in constants, not `config.toml` settings. What you can tune per job is set through the `schedule_job` parameters above; the `[fermix_core.jobs]` config block only carries delivery defaults and the network-readiness switch.

A job can also be pinned to a smaller or cheaper provider and model, independent of the main agent's route. Jobs that do not pin a provider use the cron route from `[fermix_core.routing]` (`cron_provider`, `cron_model`, `cron_reasoning_effort`), which defaults to the same chain as the main agent when unset. See [providers and models](/docs/providers-and-models) for routing details.

## Scheduler and runner internals

The scheduler is event-driven: instead of polling on a fixed interval, it arms a timer for the next due job's scheduled time and wakes exactly then, with a periodic reconciliation sweep (about once a minute) as a backstop. When a due time arrives it queries SQLite for due jobs (up to 20 at a time by default) and claims each one atomically (one claim wins, with no chance of a job being started twice), transitioning it from `scheduled` to `running`. If a claim races with another claim for the same job, only one succeeds. Each claimed job gets one `Jobs.Runner` process under a `DynamicSupervisor` (`Jobs.RunnerSupervisor`).

At most 4 scheduled runs execute concurrently. Once that ceiling is reached a due tick claims nothing further — the remaining due jobs stay `scheduled` until a slot frees, and a later tick or the reconciliation sweep claims them. The ceiling is a built-in daemon constant, not a `config.toml` key, and it gates the timed path only: a `run_job_now` trigger claims immediately regardless of how many runs are already active.

The runner:

1. Builds a prompt: a "cron guidance" system note, the skill prompt if bound, job context, the current date (in the job's timezone), and the task prompt.
2. Resolves the provider route, trust, allowed tools, and capability policy.
3. Runs `AgentLoop.run/1` in a monitored subprocess.
4. A watchdog timer (a safety timer that steps in if the run hangs) kills the subprocess on wall-clock or inactivity timeout.
5. Persists the run summary to memory (unless silent) and writes artifacts to `~/.fermix/job_runs/<run_id>/`.
6. Finalizes job state and dispatches delivery.

When multiple jobs fire in the same minute, the runner staggers each new run's first network call by 250 ms per already-active run (capped at 5 seconds) to avoid saturating the HTTP connection pool simultaneously.

### Wake-from-sleep resilience

If the host resumes from sleep and a cron run fires before the network is ready, the runner detects the connection-unavailable error and retries the entire loop with exponential backoff (waiting longer between each retry, up to 4 attempts over 60 seconds). The retry only applies while no tool has executed yet in the attempt — once a tool has run, a connection failure fails the run rather than replaying completed tool calls and their side effects. A network readiness probe (a quick connection test to the configured provider host) runs before the first model call, gated by `[fermix_core.jobs] network_readiness_enabled` (default `true`).

### Missed runs

When the daemon is offline across a recurring job's scheduled fire time, the due time is far in the past. Fermix skips the stale run rather than firing it late, and advances `next_run_at` to the next future occurrence. The staleness window is one hour (3600 seconds) — a built-in daemon default, not a `config.toml` key. One-off `once` jobs are exempt: they run late rather than being dropped.

## Trust and capability policy

Each job records the creator's trust level at creation time.

- Jobs created by an operator-trust session run at `:operator` trust. The full tool surface is available (subject to `allowed_tools` narrowing).
- Jobs created by a guest-trust session run at `:guest` trust. The capability policy is intersected with the creator's ceiling at run time and cannot be widened. The default policy for non-operator jobs is `[:read_only, :network]`.

The `schedule_job` tool does not expose a `capability_policy` parameter in its public schema; policy is derived from the caller. Only `allowed_tools` is caller-controllable.

### Delegation to sub-agents

Operator-trust runs include the `subagents` delegation tool, so a scheduled job can fan work out to sub-agent workers. Guest-trust runs never see it: `subagents` is policy class `external_api`, which the guest surface excludes.

Delegated workers run at the run's own trust, but with `read_write` and `gui_control` subtracted from their policy classes — a worker can read, browse, and run sandbox-bounded commands, but cannot mutate local state or drive the desktop. Worker routing follows the usual sub-agent precedence: when `[fermix_core.routing]` sets `subagent_provider` / `subagent_model` (or the call passes its own model), the workers use that route; otherwise they inherit the route the run itself resolved — including the job's own `provider`/`model` pin — rather than falling back to the global primary.

## Expiry

Set `expires_at` to an ISO-8601 UTC datetime to create a temporary job. When the scheduler's tick detects that `expires_at` has passed, the job is marked `expired`, `enabled` is set to `false`, and its memory source is updated to `status: expired`. No further runs fire.

## Memory sources

Each scheduled job creates a memory source entry. Run summaries are written to that source after each successful run (unless the response matches `silent_marker`). Use `memory_sources_list` to see job-backed sources, and `list_job_runs` / `get_job_run` to inspect run history. The `get_job_run` response includes the `task_prompt` the run actually executed, sourced from the run's recorded snapshot, so it reflects the instructions at run time rather than a possibly-since-edited current prompt.

## Related pages

- [Channels](/docs/channels): configuring channel targets for delivery
- [Configuration](/docs/configuration): `[fermix_core.jobs]` and `[fermix_core.routing]` config keys
- [Tool reference](/docs/tool-reference): full parameter and return value docs for all job tools
- [The agent loop](/docs/the-agent-loop): how `AgentLoop` bounds and executes each run
