# cron

> Schedule functions with cron expressions - registers the `cron` trigger type.

| field | value |
|-------|-------|
| version | 0.21.10 |
| type | binary |
| license | Apache-2.0 |
| repo | https://github.com/iii-hq/workers |
| supported_targets | x86_64-apple-darwin, aarch64-apple-darwin, i686-pc-windows-msvc, x86_64-pc-windows-msvc, aarch64-pc-windows-msvc, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-unknown-linux-musl, armv7-unknown-linux-gnueabihf |
| author | iii |

## installation

```sh
iii trigger compose::add worker=cron@0.21.10
```

## configuration

```yaml
- adapter:
    name: local
```

## dependencies

- `configuration` @ `0.x`

## readme

# cron

Schedules registered functions with cron expressions. Any function bound to a
`cron` trigger is invoked by this worker at the next matching UTC time. The
worker replaces the legacy built-in cron worker while keeping the trigger type
and payload shape stable.

## Install

```bash
iii worker add cron
```

`iii worker add` fetches the binary, writes a config block into
`~/.iii/config.yaml`, and the engine starts the worker the next time it boots.

## Quickstart

Register a function and bind it to this worker's trigger type (`cron`) with an
`expression`:

```rust
use iii_sdk::protocol::RegisterTriggerInput;
use iii_sdk::{errors::Error, register_worker, InitOptions, RegisterFunction};
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let iii = register_worker("ws://localhost:49134", InitOptions::default());

    iii.register_function(
        "jobs::tick",
        RegisterFunction::new_async(|payload: Value| async move {
            println!("cron fired: {payload}");
            Ok::<Value, Error>(json!({"ok": true}))
        }),
    );

    iii.register_trigger(RegisterTriggerInput {
        trigger_type: "cron".to_string(),
        function_id: "jobs::tick".into(),
        config: json!({ "expression": "*/5 * * * * *" }),
        metadata: None,
    })?;

    tokio::signal::ctrl_c().await?;
    Ok(())
}
```

The function receives:

```json
{
  "trigger": "cron",
  "job_id": "<trigger-id>",
  "scheduled_time": "2026-07-03T12:00:00+00:00",
  "actual_time": "2026-07-03T12:00:00.123456789+00:00"
}
```

## Configuration

| Field | Default | Description |
|---|---|---|
| `adapter.name` | `local` | Lock backend. Use `local` for process-local locking or `redis` for multi-instance mutual exclusion. |
| `adapter.config.redis_url` | `redis://localhost:6379` | Redis URL when `adapter.name` is `redis`. |

Configuration is owned by the `configuration` worker - edit it from the
console (**Configuration -> Workers -> cron**) or seed it once via
`--config <file>.yaml` on first boot. Changing the lock adapter hot-swaps the
scheduler under a serialized apply lock: existing jobs are stopped, re-created
with the new backend, and never run in two scheduler instances at once.

## Console page

While the worker is connected it injects a **cron** page into the console
(`#/ext/cron`): every agent-owned schedule with its cadence, next UTC run and
fire count, the cron bindings other workers registered for themselves, and a
composer that turns "every weekday at 09:00, summarise open PRs" into a
registered schedule. Schedules created there live in a session of their own,
so each routine keeps its own transcript.

## Trigger type

This worker always registers the `cron` trigger type. Bind a function to it
with:

| Field | Required | Default | Description |
|---|---|---|---|
| `expression` | yes | - | Cron expression parsed by the Rust `cron` crate. Six-field `sec min hour day month weekday` expressions are supported, and a seventh year field is accepted. |
| `condition_function_id` | no | - | Function invoked before the scheduled handler. Only an explicit JSON `false` return blocks the fire; missing/null/truthy returns allow it. Condition errors skip that fire. |

All schedules use UTC. Missed fires while the worker is stopped are skipped;
there is no catch-up replay.

Write the day of week as a name (`Mon` ... `Sun`). Numerically the crate counts
Sunday as 1, so `0 0 9 * * 1` fires on Sunday, not Monday.

## Console trigger activity

The worker injects a cron-specific source section into Consoles that support
`host.triggerRenderers`. Trigger registration, firing, and retirement show a
plain-language schedule, the exact expression, explicit UTC, and the optional
`condition_function_id`. Expressions that cannot be summarized without
hiding cron semantics keep an honest “custom schedule” label and the raw
expression.

The Console retains the surrounding activity, delivery target/result,
lifecycle state and controls, and raw JSON. Disabling or disconnecting this
worker's injected UI therefore falls back to the generic trigger view rather
than removing trigger activity.

For local UI development:

```bash
pnpm --dir cron/ui test
pnpm --dir cron/ui build
# terminal 1
pnpm --dir cron/ui watch
# terminal 2
cd cron && III_CRON_UI_WATCH=1 cargo run
```

The Rust build embeds `cron/page.js` and `cron/styles.css`; production does
not require a separate asset server.

### Requires removing the legacy built-in cron worker

The legacy built-in cron worker also owns the `cron` trigger type. Two owners
of the same trigger type on one engine collide - whichever registers last
wins - so this worker requires it to be absent: omit it from the
engine's `config.yaml` (a config that doesn't list a worker won't run it).

On boot, this worker queries the engine for connected workers and refuses to
start with a clear error if the legacy built-in is still active, so a stale config
fails loudly instead of silently racing the built-in worker for ownership of
`cron`.

## Parity vs builtin

| Behavior | Builtin | This worker |
|---|---|---|
| Expressions | 6-7 field (`cron` crate) | same |
| Timezone | UTC only | same |
| Missed runs | skipped, no catch-up | same |
| Condition | only explicit `false` blocks | same |
| Lock TTL | 30s | same |
| Lock backends | kv (process-local), redis | local (process-local), redis |
| Service functions | none | none |

## skills

This version ships 1 skill file for agents, also browsable on the worker page:

- [`SKILL.md`](https://workers.iii.dev/workers/cron?tab=skills&skill=SKILL.md&version=0.21.10)

### SKILL.md

# cron

The `cron` worker schedules registered functions to run on recurring cron
expressions. It exposes no callable `cron::*` functions; its entire surface is
the `cron` trigger type, bound through a worker SDK trigger registration such
as `iii.registerTrigger({ type: 'cron', function_id, config })`.

Install it with `iii worker add cron`. The legacy in-engine cron service must not
run on the same engine because it also owns the `cron` trigger type. Remove the
legacy built-in from the engine config before starting this worker; the standalone
worker refuses to boot when the builtin is active.

On every firing, the worker optionally evaluates a condition function, acquires
a lock through the configured backend, and invokes the target function with an
event payload containing `trigger`, `job_id`, `scheduled_time`, and
`actual_time`. `scheduled_time` and `actual_time` let handlers observe drift
without querying the scheduler.

The schedule grammar is the Rust `cron` crate dialect: six or seven fields,
`second minute hour day-of-month month day-of-week [year]`. The year field is
optional. The leading field is always seconds, so `0 */5 * * * *` fires every
5 minutes at second 0, while `*/5 * * * * *` fires every 5 seconds.

Write the day of week as a name: `Mon`, `Tue`, `Wed`, `Thu`, `Fri`, `Sat`,
`Sun`. Numerically this dialect counts Sunday as 1, one ahead of the Unix
convention, so `0 0 9 * * 1` fires on Sunday and a `1` written for Monday
fires a day early. Names avoid the whole question.

Two lock backends govern duplicate firing. `local` is the default and is only
process-local; every worker instance can fire the same job in a multi-instance
deployment. `redis` uses Redis locking and is required for once-only firing
across a fleet. The lock TTL is 30 seconds.

## When to Use

- A function should run periodically without a separate scheduler process or
  system crontab entry.
- You need recurring cleanup, reporting, maintenance, or batch jobs that should
  fire on UTC cron windows.
- You need once-only firing across a fleet and can configure the `redis` lock
  backend.
- A scheduled job should be conditionally skipped by setting
  `condition_function_id`, without putting the condition inside the handler.

## Boundaries

- No callable functions are exposed. Never invoke `cron::*`; bind the `cron`
  trigger type to one of your own functions.
- The default `local` backend is process-local. Do not rely on it for once-only
  jobs in a multi-instance deployment.
- The worker does not catch up missed runs after downtime. It schedules the
  next upcoming UTC fire from the time it is running.
- A condition function only blocks when it returns explicit JSON `false`.
  Truthy, null, or missing results allow the fire; condition errors skip that
  fire.
- Five-field crontab expressions are not the intended format. Include the
  seconds field.
- For data-change or stream-change reactions, use the relevant state, stream,
  queue, or event worker instead; `cron` fires on the clock only.

## Reactive triggers

Bind a `cron` trigger when a handler should run on a recurring schedule. The
handler runs through the engine as a normal function invocation. With the
`redis` backend, only one worker instance should own a scheduled fire across
the fleet.

Reach for it when:

- You need recurring execution such as cleanup, reports, or maintenance.
- You want condition-gated firing: set `condition_function_id` and the worker
  evaluates it before each run.
- One function should be driven by multiple schedules; register multiple
  triggers and branch on the `job_id` payload field.

### How to bind

1. Register a handler: `iii.registerFunction('jobs::cleanup-old-data', handler)`.
2. Register the trigger:

```typescript
iii.registerTrigger({
  type: 'cron',
  function_id: 'jobs::cleanup-old-data',
  config: {
    expression: '0 0 2 * * * *',  // sec min hour dom month dow [year]; daily at 02:00:00 UTC.
    // condition_function_id is optional.
  },
})
```

`expression` is required and must parse. Bind one `function_id` to several
triggers with distinct trigger ids to drive multiple schedules into one
handler; the trigger id arrives as `job_id` in the event. The handler's return
value is ignored.

Event payload:

```json
{
  "trigger": "cron",
  "job_id": "<trigger-id>",
  "scheduled_time": "2026-07-03T12:00:00+00:00",
  "actual_time": "2026-07-03T12:00:00.123456789+00:00"
}
```

Configure the lock backend through the central configuration entry for worker
`cron`:

```yaml
adapter:
  name: redis
  config:
    redis_url: redis://localhost:6379
```

Use `adapter.name: local` for single-process development and
`adapter.name: redis` for multi-instance once-only scheduling.

## api reference

```json
{
  "functions": [
    {
      "description": "Internal: reload cron configuration from the authoritative store on change.",
      "metadata": {},
      "name": "cron::on-config-change",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "ConfigChangeRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "ConfigChangeAck",
        "type": "object"
      }
    },
    {
      "description": "Serve the cron worker's injected console UI assets (content function for its console:script / console:style triggers).",
      "metadata": {
        "internal": true
      },
      "name": "cron::ui-content",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Input of the content function: the console asks for one asset by path.",
        "properties": {
          "path": {
            "description": "The asset path from the trigger config (e.g. `state/page.js`).",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "title": "UiContentInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Output of the content function.",
        "properties": {
          "content": {
            "description": "The asset source, verbatim.",
            "type": "string"
          },
          "content_type": {
            "description": "MIME type the console should serve the asset with.",
            "type": "string"
          }
        },
        "required": [
          "content",
          "content_type"
        ],
        "title": "UiContentResult",
        "type": "object"
      }
    }
  ],
  "triggers": [
    {
      "description": "Cron-based scheduled triggers",
      "invocation_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Trigger config schema, field-parity with the engine's CronTriggerConfig.",
        "properties": {
          "condition_function_id": {
            "description": "Optional function ID to evaluate before invoking the handler.",
            "type": [
              "string",
              "null"
            ]
          },
          "expression": {
            "description": "Cron expression (6-field format: sec min hour day month weekday; a 7th year field is accepted).",
            "type": "string"
          }
        },
        "required": [
          "expression"
        ],
        "title": "CronTriggerSpec",
        "type": "object"
      },
      "metadata": {},
      "name": "cron",
      "return_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "actual_time": {
            "description": "Actual execution time (RFC3339)",
            "type": "string"
          },
          "job_id": {
            "description": "Unique job identifier",
            "type": "string"
          },
          "scheduled_time": {
            "description": "Scheduled execution time (RFC3339)",
            "type": "string"
          },
          "trigger": {
            "description": "Always \"cron\"",
            "type": "string"
          }
        },
        "required": [
          "actual_time",
          "job_id",
          "scheduled_time",
          "trigger"
        ],
        "title": "CronCallRequest",
        "type": "object"
      }
    }
  ]
}
```
