---
name: right-cron
description: >-
  Manages cron jobs for this Right Agent agent via MCP tools. Creates, updates,
  and deletes cron specs stored in the agent database. The Rust runtime handles
  scheduling and execution automatically. Use when the user mentions cron
  jobs, scheduled tasks, reminders, one-shot tasks, or recurring tasks.
version: 3.3.0
---

# /right-cron -- Cron Job Manager

## When to Activate

Activate this skill when:
- The user mentions "cron", "cron jobs", "scheduled tasks", "reminders", or "Right Cron"
- The user asks to schedule, create, remove, or change a recurring or one-shot task
- The user asks to run something at a specific time or after a delay
- The user asks about cron run history or why a job failed

## How It Works

Cron specs are stored in the agent database. The Rust runtime reconciles specs continuously and schedules jobs automatically. Use MCP tools to manage specs — no file creation needed.

## Creating a Cron Job

**Minimum delivery latency.** A cron notification is held in the delivery
queue until the chat has been idle for **{{ idle_threshold_min }} minutes**
(UX-politeness gate — see "Triggering ≠ delivery" below). Therefore:

- Never promise the user a reminder sooner than {{ idle_threshold_min }}
  minutes from now. The scheduled `run_at` can be sooner, but the user will
  not actually see the message until the chat has been quiet for
  {{ idle_threshold_min }} minutes.
- If the user requests a reminder in less than {{ idle_threshold_min }}
  minutes, say so plainly: "I can schedule it, but it won't reach you until
  ~{{ idle_threshold_min }} minutes after you stop typing — want me to set it
  for then instead?" Then act on their choice.

`max_budget_usd` caps the dollar spend per invocation — Claude stops gracefully when the budget is reached. The default is `$2.0`, which covers most jobs including multi-step research. Only override it when the user explicitly asks for a different cap or the task is unusually expensive.

Three job types are supported:

### Recurring (default)

```
mcp__right__cron_create(
  job_name: "health-check",
  schedule: "17 9 * * 1-5",
  prompt: "Check system health and report status"
)
```

### One-shot cron (fire once at next match, then auto-delete)

```
mcp__right__cron_create(
  job_name: "deploy-check",
  schedule: "30 15 * * *",
  recurring: false,
  prompt: "Verify deployment completed successfully"
)
```

### Run at specific time (fire once, then auto-delete)

```
mcp__right__cron_create(
  job_name: "remind-deploy",
  run_at: "2026-04-15T15:30:00Z",
  prompt: "Remind the user to review PR #42"
)
```

## Writing Cron Prompts

The cron runs as a separate, non-interactive session — its only
delivery channel is its structured output. Phrase the `prompt:` as a
**task that produces text**, not as an imperative messaging action.
Imperative verbs like "send", "tag", "notify", "ping" prime the cron
agent to look for an external messaging tool.

| User said                            | Store as                                                  |
|--------------------------------------|-----------------------------------------------------------|
| "Tag @bob with a reminder about X"   | "Output a reminder about X, mentioning @bob"              |
| "Send a message to @alice at 9am"    | "Output a heads-up about X, addressed to @alice"          |
| "Ping me when Y happens"             | "Check Y. If it happened, output a notification about it" |
| "Notify the channel about Z"         | "Output a notification about Z"                           |

@username is fine as plain text — it ends up in the delivered
message and Telegram renders it as a mention. Don't strip the user's
content or schedule; only rephrase the delivery-imperative verbs.

## Editing a Cron Job

Use the `mcp__right__cron_update` MCP tool. Only pass the fields you want to change — unspecified fields keep their current values.

```
# Change only the prompt
mcp__right__cron_update(job_name: "health-check", prompt: "New check prompt")

# Change only the schedule
mcp__right__cron_update(job_name: "health-check", schedule: "43 */4 * * *")

# Switch from recurring to one-shot run_at
mcp__right__cron_update(job_name: "health-check", run_at: "2026-04-16T10:00:00Z")
```

Setting `schedule` clears `run_at`. Setting `run_at` clears `schedule` and forces `recurring=false`.

## Removing a Cron Job

Use the `mcp__right__cron_delete` MCP tool:

```
mcp__right__cron_delete(job_name: "health-check")
```

## Triggering a Cron Job Manually

Use the `mcp__right__cron_trigger` MCP tool to run a job immediately:

```
mcp__right__cron_trigger(job_name: "health-check")
```

The job is queued for the cron engine. Lock check still applies — if the job is currently running, the trigger is skipped.

**Triggering ≠ delivery.** Be honest with the user about two things:

1. The cron itself decides whether to notify. User-facing delivery requires
   `delivery.kind = "notify"` with `delivery.content`; silent runs use
   `delivery.kind = "silent"` with `delivery.reason`. Don't promise the user a
   message will arrive.
2. If the cron does produce a notification, delivery is held until the chat
   has been idle for **{{ idle_threshold_min }} minutes**. This is a
   UX-politeness gate — cron messages must not interrupt an active
   conversation. While the user is actively chatting, results queue up.

If the user wants the result without waiting, suggest they call
`mcp__right__cron_list_runs` after the job finishes — `delivery_status` and
`delivery` tell the full story.

## Listing Current Cron Jobs

Use the `mcp__right__cron_list` MCP tool to see all configured jobs:

```
mcp__right__cron_list()
```

Returns: job_name, schedule, prompt, lock_ttl, max_budget_usd, recurring, run_at for each job.

## Parameters

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `job_name` | string | Yes | - | Lowercase alphanumeric and hyphens (e.g. `health-check`). |
| `schedule` | string | Conditional | - | 5-field cron expression (minute hour day-of-month month day-of-week) in **UTC**. Required if `run_at` not set. Mutually exclusive with `run_at`. |
| `run_at` | string | Conditional | - | ISO8601 UTC datetime (e.g. `2026-04-15T15:30:00Z`). Fire once at this time, then auto-delete. Required if `schedule` not set. Mutually exclusive with `schedule`. |
| `recurring` | boolean | No | `true` | If `false` with `schedule`, fires once at next match then auto-deletes. Ignored if `run_at` is set. |
| `prompt` | string | Yes | - | The task prompt that Claude executes when the cron fires. |
| `lock_ttl` | string | No | `30m` | Duration after which a lock is considered stale (e.g. `10m`, `1h`). |
| `max_budget_usd` | number | No | `2.0` | Maximum dollar spend per invocation. Claude stops gracefully when budget is reached. |
| `target_chat_id` | integer | No | - | Telegram chat ID to deliver cron notifications to. See guidance below. |
| `target_thread_id` | integer | No | - | Message thread ID within a supergroup topic. Only relevant when `target_chat_id` is a supergroup with topics enabled. |

## Delivery Target

**Always pass `target_chat_id`** when creating a cron job in response to a user request. Set it to the `chat.id` from the incoming message YAML — this makes the cron deliver back to the same chat where the user asked for it. The runtime rejects any chat ID not in the agent's allowlist.

For supergroup topics, also pass `target_thread_id` from the message's `chat.topic_id` if the user wants notifications in that specific topic thread.

To change a cron's delivery destination, call `mcp__right__cron_update` with the new `target_chat_id` (and optionally `target_thread_id`). Only deviate from the current chat if the user explicitly requests a different destination.

Example — creating a recurring job with delivery target:

```
mcp__right__cron_create(
  job_name: "morning-briefing",
  schedule: "17 9 * * 1-5",
  prompt: "...",
  target_chat_id: 123456789
)
```

## One-Shot Job Behavior

One-shot jobs (both `run_at` and `recurring: false`) auto-delete from `cron_specs` after execution.
- `cron_list` shows pending one-shot specs until they fire
- `cron_list_runs` shows execution history (runs are preserved even after spec deletion)
- If the bot was offline when `run_at` passed, the job fires on the next startup

### Schedule Guidelines

**Never silently use :00 or :30 minutes.** These are peak times when many automated jobs fire simultaneously, causing API rate limit spikes. If the user asks for a round time (e.g. "every day at 9:00"), propose an offset like `:17` or `:43` and explain why. If the user insists on the exact round time, create it as requested.

## Checking Run History

Use the `right` MCP server tools to check cron job execution history.

### mcp__right__cron_list_runs

Returns recent runs sorted by `started_at` descending.

Parameters:
- `job_name` (optional string) — filter by job name; omit to return all jobs
- `limit` (optional integer) — max runs to return; default 20

Each run record contains: `id`, `job_name`, `started_at`, `finished_at`, `exit_code`, `status`, `log_path`, `run_note`, `delivery`, `delivered_at`, `delivery_status`

**Delivery diagnostics:**
- `delivery_status`: lifecycle state — `none` (CC decided nothing to report), `pending` (awaiting delivery), `retryable` (delivery failed and will retry), `delivered` (sent to Telegram), `superseded` (newer run replaced this one), `failed` (delivery gave up after retries)
- `delivery`: structured decision; `delivery.kind = "notify"` carries user-facing `delivery.content`, while `delivery.kind = "silent"` carries `delivery.reason`
- `run_note`: technical run history/debug metadata; never delivered to Telegram
- `delivered_at`: timestamp when the result was delivered, or null

### mcp__right__cron_show_run

Returns full metadata for a single run.

Parameters:
- `run_id` (string, UUID) — run to retrieve

### Reading logs

The `log_path` field in each run record points to the NDJSON log file inside the agent's working directory. Read the tail to see recent activity:

```
Read(file_path: "<log_path>")
```

For large logs, prefer reading just the tail — use a high `offset` value to skip to the end.

### Debugging example

```
User: "Why did morning-briefing fail?"

1. mcp__right__cron_list_runs(job_name="morning-briefing", limit=5)
   -> Find the failed run (status="failed")
2. mcp__right__cron_show_run(run_id="<run_id from step 1>")
   -> Get full metadata including log_path
3. Read(file_path: "<log_path>")
   -> Read the tail of the log to diagnose the failure
```

### Diagnosing missing notifications

When the user asks "why wasn't I notified?", check `delivery_status` and `delivery`:

```
1. mcp__right__cron_list_runs(job_name="github-tracker", limit=5)
   -> Check delivery_status for each run:
      - "none" + delivery.kind = "silent" → CC decided nothing to report, delivery.reason explains why
      - "pending" → notification waiting for chat idle ({{ idle_threshold_min }} min threshold)
      - "retryable" → delivery failed and will retry
      - "superseded" → newer run replaced this one before delivery
      - "failed" → delivery failed after 3 attempts, check logs
      - "delivered" → was sent to Telegram successfully
```

Never guess at delivery issues — always check the actual `delivery_status` field.

## Watching a Running Job

To see what a cron job is currently doing:

1. Find the running job:
```
mcp__right__cron_list_runs(job_name="health-check", limit=1)
```
Check the `status` field — `"running"` means the job is active.

2. Read the tail of the log file to see current activity:
```
Read(file_path: "<log_path from step 1>")
```
The log is NDJSON (one JSON event per line) — look for `"type": "assistant"` events to see what the job is doing.

3. To follow progress, read the tail again after some time has passed.

## Constraints

1. **UTC schedules**: Cron expressions are evaluated in UTC by the Rust runtime.
2. **Continuous reconciliation**: The runtime reconciles specs continuously; create/update/delete take effect promptly. Don't quote a specific number of seconds to the user — the polling interval is an internal detail.
3. **Manual triggers**: `mcp__right__cron_trigger` queues the job for the next reconciliation pass. If the job is locked (still running from a previous invocation), the trigger is skipped.
4. **Conditional delivery**: A successful run does not guarantee a Telegram message. The cron's structured output decides via `delivery.kind`: `"notify"` sends `delivery.content`, while `"silent"` records `delivery.reason`. Pending notifications wait for {{ idle_threshold_min }} minutes of chat idle before they are sent.
