---
title: Cron API
description: "Manage cron jobs and automated platform tasks"
---

# Cron API

Create, list, and delete cron jobs directly on the OpenClaw gateway, and trigger platform-level automated tasks. Cron jobs run recurring tasks on your agent using the gateway's built-in scheduler.

<Note>These endpoints manage cron jobs on the gateway itself via `POST /tools/invoke`. For application-level scheduled tasks stored in the database, see the [scheduled tasks API](/api-reference/scheduled-tasks).</Note>

<Note>Cron is enabled by default on all new instances. The provisioning template sets a maximum of 2 concurrent runs and 24-hour session retention. You can adjust these limits using the [config API](/api-reference/config).</Note>

All gateway cron endpoints require session authentication. The blog-daily endpoint uses bearer-token authentication with `CRON_SECRET`.

## List cron jobs

```http
GET /api/cron
```

Returns all cron jobs from the gateway, including disabled jobs.

### Response

```json
{
  "jobs": [
    {
      "id": "heartbeat",
      "name": "Heartbeat",
      "enabled": true,
      "schedule": {
        "kind": "every",
        "everyMs": 1800000
      },
      "payload": {
        "kind": "systemEvent",
        "text": "Heartbeat check — review emails, calendar, and recent activity."
      },
      "lastRun": "2026-03-30T01:00:00Z",
      "nextRun": "2026-03-30T01:30:00Z"
    }
  ],
  "total": 1,
  "source": "gateway"
}
```

### Job object

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Job identifier |
| `name` | string | Job display name |
| `enabled` | boolean | Whether the job is active |
| `schedule` | object | Schedule configuration. Contains `kind` (`every` for interval-based) and timing fields like `everyMs` (interval in milliseconds) or `expr` (cron expression). |
| `payload` | object | Job payload sent to the agent when the job runs |
| `lastRun` | string \| null | ISO 8601 timestamp of the last execution |
| `nextRun` | string \| null | ISO 8601 timestamp of the next scheduled execution |

### Response fields

| Field | Type | Description |
|-------|------|-------------|
| `jobs` | array | List of cron job objects |
| `total` | number | Total number of jobs |
| `source` | string | Data source — `gateway` on success, `gateway-error` when the gateway is unreachable |

### Gateway errors

When the gateway is unreachable, the endpoint returns HTTP `200` with an empty job list and the error detail:

```json
{
  "jobs": [],
  "error": "Gateway unreachable",
  "source": "gateway-error"
}
```

### Example

```bash
curl -X GET https://agentbot.sh/api/cron \
  -H "Cookie: next-auth.session-token=YOUR_SESSION"
```

## Create a cron job

```http
POST /api/cron
```

Adds a new cron job to the gateway.

### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Job name |
| `schedule` | object | Yes | Schedule configuration. Use `{ "kind": "every", "everyMs": 3600000 }` for interval-based schedules, or `{ "kind": "cron", "expr": "0 9 * * *" }` for cron expressions. |
| `payload` | object | Yes | Job payload sent to the agent on each run |
| `enabled` | boolean | No | Whether the job starts active. Defaults to `true`. |

### Example request

```json
{
  "name": "Daily digest",
  "schedule": {
    "kind": "every",
    "everyMs": 86400000
  },
  "payload": {
    "kind": "systemEvent",
    "text": "Generate a daily activity digest."
  }
}
```

### Response

```json
{
  "success": true,
  "source": "gateway"
}
```

### Errors

| Code | Description |
|------|-------------|
| 400 | `name, schedule, and payload required` — one or more required fields are missing |
| 401 | Unauthorized — no valid session |
| 502 | Gateway error — the gateway rejected the request or is unreachable |

### Example

```bash
curl -X POST https://agentbot.sh/api/cron \
  -H "Content-Type: application/json" \
  -H "Cookie: next-auth.session-token=YOUR_SESSION" \
  -d '{
    "name": "Daily digest",
    "schedule": { "kind": "every", "everyMs": 86400000 },
    "payload": { "kind": "systemEvent", "text": "Generate a daily activity digest." }
  }'
```

## Delete a cron job

```http
DELETE /api/cron?jobId=heartbeat
```

Removes a cron job from the gateway.

### Query parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `jobId` | string | Yes | ID of the job to delete |

### Response

```json
{
  "success": true,
  "source": "gateway"
}
```

### Errors

| Code | Description |
|------|-------------|
| 400 | `jobId required` — the `jobId` query parameter is missing |
| 401 | Unauthorized — no valid session |
| 502 | Gateway error — the gateway rejected the request or is unreachable |

### Example

```bash
curl -X DELETE "https://agentbot.sh/api/cron?jobId=heartbeat" \
  -H "Cookie: next-auth.session-token=YOUR_SESSION"
```

---

## Daily blog publish

```http
GET /api/cron/blog-daily
```

Generates and publishes a daily operations brief to the auto-blog. This endpoint is designed to be called by a Vercel cron schedule (hourly) but only publishes when the current hour is **9 AM Europe/London**, ensuring the post time does not drift during daylight-saving changes.

Posts are stored in KV (Upstash Redis) and appear on the blog index alongside static posts.

<Note>This endpoint is invoked automatically by the Vercel cron scheduler. You do not need to call it manually unless you want to force-publish outside the normal window.</Note>

### Authentication

Requires a `CRON_SECRET` bearer token in the `Authorization` header.

```
Authorization: Bearer <CRON_SECRET>
```

### Query parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `force` | string | No | Set to `"1"` to publish regardless of the current hour |

### Response

On successful publish:

```json
{
  "success": true,
  "slug": "daily-ops-2026-04-09",
  "publishedAt": "2026-04-09T08:00:00.000Z"
}
```

When skipped because the current hour is outside the publish window:

```json
{
  "skipped": true,
  "reason": "outside_publish_window",
  "targetHour": 9,
  "timezone": "Europe/London",
  "isoDate": "2026-04-09"
}
```

When skipped because today's post was already published:

```json
{
  "success": true,
  "skipped": true,
  "reason": "already_published",
  "slug": "daily-ops-2026-04-09"
}
```

### Response fields

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | `true` when the request completed without error |
| `skipped` | boolean | `true` when no new post was created |
| `reason` | string | Why the post was skipped — `outside_publish_window` or `already_published` |
| `slug` | string | Slug of the published or existing post (format `daily-ops-YYYY-MM-DD`) |
| `publishedAt` | string | ISO 8601 timestamp of the publish time (only on new publishes) |
| `targetHour` | number | The London-local hour the cron targets (always `9`) |
| `timezone` | string | Timezone used for the publish window (`Europe/London`) |
| `isoDate` | string | Current date in `YYYY-MM-DD` format |

### Errors

| Code | Description |
|------|-------------|
| 401 | `Unauthorized` — missing or invalid `CRON_SECRET` bearer token |

### Example

Force-publish today's daily blog post:

```bash
curl -X GET "https://agentbot.sh/api/cron/blog-daily?force=1" \
  -H "Authorization: Bearer YOUR_CRON_SECRET"
```

---

## Broadcast scheduler

```http
GET /api/cron/broadcast
```

Finds mixtapes and ad campaigns due to broadcast, creates Mux live streams, and triggers FFmpeg-based broadcasting via the platform OpenClaw runtime. Runs every 5 minutes via Vercel Cron with a 5-minute look-ahead window.

When the OpenClaw runtime is unavailable, the endpoint generates a ready-to-use FFmpeg command and sends an admin alert so the broadcast can be triggered manually.

### Authentication

Requires a `CRON_SECRET` bearer token in the `Authorization` header.

```
Authorization: Bearer <CRON_SECRET>
```

### Broadcast sources

The scheduler checks two sources for items due to broadcast:

- **Mixtapes** — records with status `scheduled` and `scheduled_at` within the look-ahead window that have a `playback_id`
- **Ad campaigns** — records with status `approved`, `starts_at` within the look-ahead window, a `playback_id`, and remaining broadcast slots

### Response

When no broadcasts are due:

```json
{
  "checked": true,
  "jobs": 0,
  "ts": "2026-04-12T10:00:00.000Z"
}
```

When broadcasts are processed:

```json
{
  "checked": true,
  "jobs": 2,
  "results": [
    { "id": "clxyz456def", "kind": "mixtape", "outcome": "triggered" },
    { "id": "clxyz789ghi", "kind": "ad", "outcome": "needs_operator" }
  ],
  "ts": "2026-04-12T10:00:00.000Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `checked` | boolean | Always `true` |
| `jobs` | number | Number of broadcast jobs processed |
| `results` | array | Per-job results (only present when `jobs > 0`) |
| `results[].id` | string | Mixtape or campaign identifier |
| `results[].kind` | string | Source type: `mixtape` or `ad` |
| `results[].outcome` | string | Result: `triggered` (broadcast started via OpenClaw), `needs_operator` (requires manual FFmpeg), or `error: <message>` |
| `ts` | string | ISO 8601 timestamp of the cron run |

### Errors

| Code | Description |
|------|-------------|
| 401 | `Unauthorized` — missing or invalid `CRON_SECRET` bearer token |
| 500 | `Mux not configured` — Mux credentials are missing |

<Note>When a broadcast job fails, the scheduler rolls back the database status to its previous state and cleans up the Mux live stream. The job appears in `results` with an `error:` outcome.</Note>

### Example

```bash
curl -X GET https://agentbot.sh/api/cron/broadcast \
  -H "Authorization: Bearer YOUR_CRON_SECRET"
```

---

## Verify X ownership claims

```http
GET /api/cron/verify-x-claims
```

Automatically verifies pending X (Twitter) ownership claims by searching the X API for challenge codes. Runs hourly via Vercel Cron. For each pending claim, the endpoint searches recent tweets for the challenge code. When a match is found, the claim is approved and the linked agent receives `verified` status with a trust score increase of 50 points. Claims that have passed their expiry date are marked as `expired`.

Each run processes up to 10 pending claims (oldest first) to stay within X API rate limits.

<Note>This endpoint is invoked automatically by the Vercel cron scheduler. You do not need to call it manually. For details on starting a verification claim, see the [Social API verification section](/api-reference/social#verification).</Note>

### Authentication

Requires a `CRON_SECRET` bearer token in the `Authorization` header.

```
Authorization: Bearer <CRON_SECRET>
```

### Environment variables

| Variable | Required | Description |
|----------|----------|-------------|
| `CRON_SECRET` | Yes | Shared secret checked by Vercel to authorize cron requests |
| `X_API_BEARER_TOKEN` | Yes | Twitter/X API v2 Bearer Token used to search recent tweets |

When `X_API_BEARER_TOKEN` is not configured, the endpoint skips processing and returns a `skipped` response.

### Response

On successful run:

```json
{
  "checked": 10,
  "verified": 2,
  "expired": 1,
  "remaining": 7
}
```

When `X_API_BEARER_TOKEN` is not configured:

```json
{
  "skipped": true,
  "reason": "X_API_BEARER_TOKEN not configured"
}
```

### Response fields

| Field | Type | Description |
|-------|------|-------------|
| `checked` | number | Total number of pending claims processed in this run (max 10) |
| `verified` | number | Number of claims that were successfully verified |
| `expired` | number | Number of claims that were marked as expired |
| `remaining` | number | Number of claims that are still pending (not yet verified or expired) |
| `skipped` | boolean | `true` when the run was skipped due to missing configuration |
| `reason` | string | Why the run was skipped |

### Claim lifecycle

When a claim is verified, the following updates are applied in a single transaction:

1. The claim's status is set to `verified` and `verifiedAt` is recorded.
2. The linked agent's `verificationStatus` is set to `verified`.
3. The linked agent's `trustScore` is incremented by 50.

When a claim has passed its `expiresAt` timestamp, it is marked as `expired` and no further verification attempts are made.

### Errors

| Code | Description |
|------|-------------|
| 401 | `Unauthorized` — missing or invalid `CRON_SECRET` bearer token |

### Example

```bash
curl -X GET https://agentbot.sh/api/cron/verify-x-claims \
  -H "Authorization: Bearer YOUR_CRON_SECRET"
```

---

## Weekly MoltX update

```http
GET /api/cron/moltx-weekly
```

Generates and publishes a weekly platform summary to MoltX. The post includes live agent counts, installed skills, service health, and recent blog highlights. Content is automatically trimmed to stay within the MoltX 500-character post limit.

Posts are deduplicated by ISO week key (for example `2026-W15`). If the current week has already been posted, the endpoint returns a success response with `skipped: true` unless you pass `force=1`.

<Note>This endpoint requires the `MOLTX_API_KEY` environment variable. When the key is not configured, the endpoint returns the generated content without posting.</Note>

### Authentication

Accepts either of:

- A `CRON_SECRET` bearer token in the `Authorization` header.
- An authenticated admin session cookie.

```
Authorization: Bearer <CRON_SECRET>
```

### Query parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `force` | string | No | Set to `"1"` to re-post even if this week was already published |
| `dryRun` | string | No | Set to `"1"` to return the generated content without posting to MoltX |

### Response

On successful publish:

```json
{
  "success": true,
  "weekKey": "2026-W15",
  "content": "Weekly Agentbot/baseFM update\n50+ deployed / 12 live agents\n...",
  "response": { "id": "post_abc123" }
}
```

When skipped because this week was already posted:

```json
{
  "success": true,
  "skipped": true,
  "reason": "already_posted",
  "weekKey": "2026-W15",
  "existing": { "postedAt": "2026-04-07T09:00:00.000Z", "weekKey": "2026-W15" }
}
```

When running in dry-run mode:

```json
{
  "success": true,
  "dryRun": true,
  "weekKey": "2026-W15",
  "content": "Weekly Agentbot/baseFM update\n..."
}
```

When `MOLTX_API_KEY` is not configured:

```json
{
  "success": false,
  "skipped": true,
  "reason": "missing_moltx_api_key",
  "weekKey": "2026-W15",
  "content": "Weekly Agentbot/baseFM update\n..."
}
```

### Response fields

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | `true` when the post was published or the request completed without error |
| `skipped` | boolean | `true` when no new post was created |
| `reason` | string | Why the post was skipped — `already_posted` or `missing_moltx_api_key` |
| `weekKey` | string | ISO week identifier (format `YYYY-Www`) |
| `content` | string | The generated post content |
| `dryRun` | boolean | `true` when the request was a dry run |
| `response` | object | The response body from the MoltX API (only on successful publish) |
| `existing` | object | Previous post state for this week (only when `reason` is `already_posted`) |

### Errors

| Code | Description |
|------|-------------|
| 401 | `Unauthorized` — missing or invalid `CRON_SECRET` bearer token and no admin session |
| 502 | `moltx_post_failed` — the MoltX API rejected the post. The response includes the upstream `status` and `body`. |

### Example

Dry-run to preview this week's post:

```bash
curl -X GET "https://agentbot.sh/api/cron/moltx-weekly?dryRun=1" \
  -H "Authorization: Bearer YOUR_CRON_SECRET"
```

Force-publish this week's update:

```bash
curl -X GET "https://agentbot.sh/api/cron/moltx-weekly?force=1" \
  -H "Authorization: Bearer YOUR_CRON_SECRET"
```
