Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,23 @@ done_hold_ms = 1500
error_hold_ms = 2500

# --- Scheduled messages (config-driven cron) ---
# Each entry sends a message to the agent at the specified schedule.
# The agent processes it and replies to the target channel.
# Everything cron-related lives under [cron].

# [[cronjobs]]
# [cron]
# usercron_enabled = true # enable hot-reload (default: false)
# usercron_path = "cronjob.toml" # relative to $HOME, or absolute

# [[cron.jobs]]
# enabled = true # optional, default: true
# schedule = "0 9 * * 1-5" # weekdays at 9:00 AM
# channel = "123456789" # target channel/thread ID
# message = "summarize yesterday's merged PRs" # prompt for the agent
# platform = "discord" # "discord" or "slack"
# sender_name = "DailyOps" # attribution (default: "openab-cron")
# timezone = "America/New_York" # IANA timezone (default: "UTC")
# timezone = "America/New_York" # IANA timezone (default: "UTC")
# thread_id = "" # optional: post to existing thread

# [[cronjobs]]
# [[cron.jobs]]
# schedule = "0 0 * * 0"
# channel = "123456789"
# message = "generate weekly status report"
41 changes: 28 additions & 13 deletions docs/config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,29 +185,42 @@ Speech-to-text transcription for voice messages. Uses an OpenAI-compatible `/aud

---

## `[[cronjobs]]`
## `[cron]`

Scheduled messages — config-driven cron. Each entry sends a message to the agent at the specified schedule, as if a user typed it. The agent processes the message and replies to the target channel.
Everything cron-related lives under `[cron]`.

```toml
[[cronjobs]]
[cron]
usercron_enabled = true # enable hot-reload (default: false)
usercron_path = "cronjob.toml" # relative to $HOME, or absolute

[[cron.jobs]]
enabled = true # optional, default: true
schedule = "0 9 * * 1-5" # cron expression (5-field POSIX)
channel = "123456789" # target channel/thread ID
message = "summarize yesterday's merged PRs" # message sent to agent
platform = "discord" # optional, default: "discord"
sender_name = "DailyOps" # optional, default: "openab-cron"
timezone = "America/New_York" # optional, default: "UTC"
timezone = "America/New_York" # optional, default: "UTC"
thread_id = "" # optional, post to existing thread

[[cronjobs]]
[[cron.jobs]]
schedule = "0 0 * * 0"
channel = "123456789"
message = "generate weekly status report"
platform = "discord"
timezone = "UTC"
```

### `[cron]` fields

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `usercron_enabled` | bool | `false` | Enable usercron hot-reload. Must be explicitly set to `true`. |
| `usercron_path` | string | — | Path to the external `cronjob.toml`. Relative paths resolve from `$HOME`. |

### `[[cron.jobs]]` fields

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `true` | Set `false` to disable without removing the entry. |
Expand All @@ -219,6 +232,8 @@ timezone = "UTC"
| `timezone` | string | `"UTC"` | IANA timezone for schedule evaluation (e.g. `"America/New_York"`, `"Europe/Berlin"`). |
| `thread_id` | string | `""` | Optional thread ID to post into an existing thread. |

The external `cronjob.toml` uses `[[jobs]]` (same fields). See [Usercron docs](cronjob.md#usercron--hot-reload-with-cronjobtoml) for details.

**Cron expression format:**

```
Expand Down Expand Up @@ -276,14 +291,14 @@ Key mapping (`values.yaml` → `config.toml`):
| `agents.<name>.pool.sessionTtlHours` | `[pool] session_ttl_hours` |
| `agents.<name>.reactions.enabled` | `[reactions] enabled` |
| `agents.<name>.stt.apiKey` | `[stt] api_key` |
| `agents.<name>.cronjobs[].enabled` | `[[cronjobs]] enabled` |
| `agents.<name>.cronjobs[].schedule` | `[[cronjobs]] schedule` |
| `agents.<name>.cronjobs[].channel` | `[[cronjobs]] channel` |
| `agents.<name>.cronjobs[].message` | `[[cronjobs]] message` |
| `agents.<name>.cronjobs[].platform` | `[[cronjobs]] platform` |
| `agents.<name>.cronjobs[].senderName` | `[[cronjobs]] sender_name` |
| `agents.<name>.cronjobs[].timezone` | `[[cronjobs]] timezone` |
| `agents.<name>.cronjobs[].threadId` | `[[cronjobs]] thread_id` |
| `agents.<name>.cronjobs[].enabled` | `[[cron.jobs]] enabled` |
| `agents.<name>.cronjobs[].schedule` | `[[cron.jobs]] schedule` |
| `agents.<name>.cronjobs[].channel` | `[[cron.jobs]] channel` |
| `agents.<name>.cronjobs[].message` | `[[cron.jobs]] message` |
| `agents.<name>.cronjobs[].platform` | `[[cron.jobs]] platform` |
| `agents.<name>.cronjobs[].senderName` | `[[cron.jobs]] sender_name` |
| `agents.<name>.cronjobs[].timezone` | `[[cron.jobs]] timezone` |
| `agents.<name>.cronjobs[].threadId` | `[[cron.jobs]] thread_id` |

> ⚠️ Use `--set-string` (not `--set`) for Discord/Slack IDs to avoid float64 precision loss:
> ```bash
Expand Down
138 changes: 129 additions & 9 deletions docs/cronjob.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Send recurring prompts to your agent on a schedule — daily summaries, weekly r

## How It Works

1. Define `[[cronjobs]]` entries in `config.toml`
1. Define `[[cron.jobs]]` entries in `config.toml`
2. OpenAB's internal scheduler evaluates cron expressions once per minute
3. When a schedule matches, the message is sent to the agent as if a user typed it
4. The agent processes the message and replies to the target channel
Expand All @@ -16,7 +16,7 @@ No external scheduler (K8s CronJob, GitHub Actions) is needed for simple use cas
Add to your `config.toml`:

```toml
[[cronjobs]]
[[cron.jobs]]
schedule = "0 9 * * 1-5"
channel = "123456789012345678"
message = "summarize yesterday's merged PRs"
Expand All @@ -26,10 +26,10 @@ This sends `summarize yesterday's merged PRs` to the agent every weekday at 09:0

## Configuration

Each `[[cronjobs]]` entry supports these fields:
Each `[[cron.jobs]]` entry supports these fields:

```toml
[[cronjobs]]
[[cron.jobs]]
enabled = true # optional, default: true
schedule = "0 9 * * 1-5" # required: cron expression
channel = "123456789012345678" # required: target channel ID
Expand Down Expand Up @@ -80,7 +80,7 @@ Standard 5-field POSIX cron, same as Linux crontab, K8s CronJob, and GitHub Acti
By default, schedules are evaluated in UTC. Set `timezone` to any IANA timezone:

```toml
[[cronjobs]]
[[cron.jobs]]
schedule = "0 9 * * 1-5"
channel = "123456789012345678"
message = "good morning team, here's today's agenda"
Expand All @@ -91,23 +91,23 @@ This fires at 09:00 New York time (13:00 or 14:00 UTC depending on DST).

## Multiple Jobs

Define as many `[[cronjobs]]` entries as you need:
Define as many `[[cron.jobs]]` entries as you need:

```toml
[[cronjobs]]
[[cron.jobs]]
schedule = "0 9 * * 1-5"
channel = "123456789012345678"
message = "summarize yesterday's merged PRs"
sender_name = "DailyOps"
timezone = "America/New_York"

[[cronjobs]]
[[cron.jobs]]
schedule = "0 0 * * 0"
channel = "123456789012345678"
message = "generate weekly status report"
sender_name = "WeeklyReport"

[[cronjobs]]
[[cron.jobs]]
schedule = "0 18 * * 1-5"
channel = "C0123456789"
message = "check for any critical alerts in the last 8 hours"
Expand Down Expand Up @@ -140,6 +140,124 @@ agents:
> --set-string agents.kiro.cronjobs[0].channel="123456789012345678"
> ```

## Usercron — Hot-Reload with `cronjob.toml`

Cronjobs defined in `config.toml` require a redeploy to change. **Usercron** lets you manage schedules in a separate `cronjob.toml` file that the scheduler hot-reloads automatically — no restart needed.

### Enable Usercron

Add to your `config.toml`:

```toml
[cron]
usercron_enabled = true
usercron_path = "cronjob.toml"
```

Usercron is **disabled by default**. Both fields are required to activate it.

#### Minimal config.toml example

```toml
[discord]
bot_token = "${DISCORD_BOT_TOKEN}"

[agent]
command = "kiro-cli"
args = ["acp", "--trust-all-tools"]
working_dir = "/home/agent"

[cron]
usercron_enabled = true
usercron_path = "cronjob.toml" # → $HOME/cronjob.toml
```

> Note: Everything cron-related lives under `[cron]` — both usercron settings and baseline `[[cron.jobs]]`.

The path is relative to `$HOME` (e.g. `"cronjob.toml"` resolves to `$HOME/cronjob.toml`). Absolute paths are used as-is. The scheduler starts watching immediately, even if the file doesn't exist yet.

### Create `cronjob.toml`

Same format as `[[cron.jobs]]` in config.toml, but uses `[[jobs]]`:

```toml
[[jobs]]
schedule = "* * * * *"
channel = "1490282656913559673"
message = "ping"
platform = "discord"
sender_name = "usercron"
timezone = "Asia/Taipei"

[[jobs]]
schedule = "0 9 * * 1-5"
channel = "1490282656913559673"
message = "summarize yesterday's merged PRs"
sender_name = "DailyOps"
timezone = "Asia/Taipei"
```

### How It Works

```
config.toml $HOME/cronjob.toml
┌──────────────────┐ ┌──────────────────────┐
│ [cron] │ │ [[jobs]] │
│ usercron_enabled │ │ schedule = "* * * *" │
│ = true │ │ channel = "123..." │
│ usercron_path │ │ message = "ping" │
│ = "cronjob.toml│" └──────────┬───────────┘
│ │ │
│ [[cron.jobs]] │ Agent writes here
│ (baseline jobs) │ anytime (mobile/CLI)
└────────┬─────────┘ │
│ │
┌────────▼─────────┐ │
│ OAB Scheduler │◄──────────────────────────┘
│ (ticks every │ check mtime every tick
│ 1 minute) │ reload if changed
└────────┬─────────┘
┌──────────────┼──────────────┐
│ │ │
baseline jobs usercron jobs should_fire()?
(immutable) (hot-reload) │
│ │ ┌────▼────┐
└──────────────┘ no── │ match? │ ──yes──► fire_cronjob()
└─────────┘ → send message
→ create thread
→ agent processes
```

1. Every scheduler tick (~1 minute), the file's modification time is checked
2. If the file changed → re-parse and replace the dynamic job list
3. `config.toml` `[[cron.jobs]]` are the **immutable baseline**; `cronjob.toml` jobs are the **dynamic overlay**
4. Invalid TOML or bad entries are logged and skipped — baseline jobs are never affected
5. Deleting the file removes all dynamic jobs (baseline jobs continue)

### Agent-Managed Schedules

Because `cronjob.toml` is a plain file, your agent can write to it directly:

```
User: set up a cronjob that pings me every minute
Agent: ✅ Written to cronjob.toml, takes effect within 1 minute
```

This enables mobile-friendly schedule management — talk to your agent from your phone, and it updates the cron file for you.

### Kubernetes Deployment

Mount `cronjob.toml` on a PVC so it persists across pod restarts, and set `usercron_path` in your config.toml:

```toml
# config.toml
[cron]
usercron_enabled = true
# Relative to $HOME — resolves to $HOME/cronjob.toml
usercron_path = "cronjob.toml"
```

## Behaviors

- **Minute-aligned**: The scheduler aligns to minute boundaries (`:00`), so `0 9 * * *` fires at exactly 09:00:00, not at whatever second the process started.
Expand Down Expand Up @@ -181,3 +299,5 @@ See [Kubernetes CronJob Reference Architecture](cronjob_k8s_refarch.md) for the
| Wrong time | Timezone mismatch | Set `timezone` explicitly (default is UTC) |
| Job skipped | Previous execution still running | Check logs for `skipping cronjob, previous execution still running` |
| Channel not found | Bot not in channel | Invite the bot to the target channel |
| Usercron not reloading | File not saved / wrong path | Check logs for `usercron file changed, reloading` |
| Usercron parse error | Invalid TOML syntax | Check logs for `failed to parse usercron file` |
14 changes: 13 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,19 @@ pub struct Config {
#[serde(default)]
pub markdown: MarkdownConfig,
#[serde(default)]
pub cronjobs: Vec<CronJobConfig>,
pub cron: CronConfig,
}

#[derive(Debug, Clone, Default, Deserialize)]
pub struct CronConfig {
/// Enable usercron hot-reload (default: false). Must be explicitly set to true.
#[serde(default)]
pub usercron_enabled: bool,
/// Path to an external cronjob.toml for hot-reloadable user-managed schedules.
pub usercron_path: Option<String>,
/// Baseline cronjob definitions: `[[cron.jobs]]`
#[serde(default)]
pub jobs: Vec<CronJobConfig>,
}

#[derive(Debug, Clone, Deserialize)]
Expand Down
Loading
Loading