# cron

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

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

## installation

```sh
iii worker add cron@0.21.11-rc.1
```

## 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 |

## 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"
      }
    }
  ]
}
```
