DocsPlatformScheduled Jobs (Crons)

Scheduled Jobs (Crons)

Run jobs on a recurring schedule. Powered by pg_cron. The Core manages scheduling, overlap detection, and delivery to your worker via the same Job Queue.


How it works

When you create a cron, the Core registers it with pg_cron inside PostgreSQL. On each tick, pg_cron calls a function (rootcx_system.enqueue_cron) that enqueues a job into the same pgmq queue used by one-shot jobs. The scheduler picks it up and dispatches it to your worker's onJob handler.

Your worker does not need to know whether a job came from a cron or a manual enqueue. The payload contains a cron_id field that identifies it as a cron job. Dispatch is otherwise identical.


Schedule syntax

Two formats are supported:

Standard cron (5 fields: minute hour day-of-month month day-of-week):

* * * * *       # every minute
*/5 * * * *     # every 5 minutes
0 9 * * *       # daily at 9 AM
0 0 * * 1-5     # midnight on weekdays
0 0 1,15 * *    # 1st and 15th of each month
0 0 $ * *       # midnight on the last day of each month

Seconds interval (for sub-minute scheduling, 1-59):

10 seconds      # every 10 seconds
30 seconds      # every 30 seconds
1 seconds       # every second

All times are GMT unless you pass a timezone parameter.

Validation rules

  • Standard cron: exactly 5 whitespace-separated fields. Only digits, *, /, -, ,, $ allowed.
  • Seconds interval: a number 1-59 followed by seconds.
  • Named shortcuts (@daily, @hourly) are not supported.
  • Alphabetic day/month names (MON, JAN) are not supported.

Overlap policy

Controls what happens when a cron fires but the previous job is still in the queue.

Policy Behavior
skip (default) Do not enqueue if a job with the same cron_id is already in the queue. Prevents pile-up.
queue Enqueue regardless. Jobs may stack up.

Declare crons in the manifest

Crons can be declared directly in manifest.json. They are synced on every deploy (created, updated, or deleted to match the manifest):

{
  "crons": [
    {
      "name": "daily-check",
      "schedule": "0 9 * * *",
      "timezone": "America/New_York",
      "method": "onDailyCheck",
      "payload": { "task": "check_replies" },
      "overlapPolicy": "skip"
    }
  ]
}
Field Required Default Description
name Yes Unique name within the app. 1-64 chars, alphanumeric/dash/underscore.
schedule Yes Cron expression or seconds interval.
timezone No GMT Timezone for the schedule.
method No If set, added to the payload as payload.method. Useful for routing in onJob.
payload No {} JSON object passed to the worker.
overlapPolicy No "skip" "skip" or "queue".

On deploy, the Core syncs the manifest crons: new entries are created, changed entries are updated, entries removed from the manifest are deleted.


Create crons at runtime

POST /api/v1/apps/{appId}/crons

Requires app:{appId}:cron.write permission.

{
  "name": "daily-check",
  "schedule": "0 9 * * *",
  "timezone": "America/New_York",
  "payload": { "task": "check_replies" },
  "overlapPolicy": "skip"
}

Response (201):

{
  "id": "f47ac10b-...",
  "appId": "crm",
  "name": "daily-check",
  "schedule": "0 9 * * *",
  "timezone": "America/New_York",
  "payload": { "task": "check_replies" },
  "overlapPolicy": "skip",
  "enabled": true,
  "pgJobId": 7,
  "createdBy": "a1b2c3d4-...",
  "createdAt": "2024-12-01T10:30:00Z",
  "updatedAt": "2024-12-01T10:30:00Z"
}

Update a cron

PATCH /api/v1/apps/{appId}/crons/{id}

Requires app:{appId}:cron.write permission. Only the owner or a user with app:{appId}:cron.manage_others can update another user's cron.

All fields optional:

{ "schedule": "*/5 * * * *", "enabled": false, "overlapPolicy": "queue" }

When enabled is set to false, the pg_cron job is unscheduled. When re-enabled, it is rescheduled.


Delete a cron

DELETE /api/v1/apps/{appId}/crons/{id}

Requires app:{appId}:cron.write permission plus ownership (or manage_others). Unschedules the pg_cron job and removes the record.


List crons

GET /api/v1/apps/{appId}/crons

Requires app:{appId}:cron.read permission. Returns all crons for the app.

GET /api/v1/crons

Lists crons across all apps the user has access to. Users with cron.manage_others see all crons for that app. Others see only their own.


Trigger a cron manually

POST /api/v1/apps/{appId}/crons/{id}/trigger

Requires app:{appId}:cron.trigger permission. Fires the cron once immediately, bypassing the schedule. Enqueues a job with the cron's payload. Returns the enqueued job's message ID:

{ "msgId": 42 }

List cron runs

GET /api/v1/apps/{appId}/crons/{id}/runs?limit=50

Requires app:{appId}:cron.read permission. Returns execution history from pg_cron's cron.job_run_details:

[
  {
    "runid": 101,
    "jobPid": 12345,
    "status": "succeeded",
    "returnMessage": "1 row",
    "startTime": "2024-12-01T09:00:00Z",
    "endTime": "2024-12-01T09:00:01Z"
  }
]

Default limit: 50, max: 500.


Handle cron jobs in your backend

Cron jobs arrive in the onJob handler, same as regular jobs. The payload contains everything you set at creation time plus a cron_id field added by the Core:

serve({}, {
  onJob: async (payload, caller, ctx) => {
    if (payload.task === "check_replies") {
      // your logic here
      return { checked: true };
    }
  },
});

Return a value to mark the job as completed. Throw to mark it as failed.


Cron-triggered agents

If the app is an AI agent, the scheduler detects this and invokes the agent instead of dispatching to onJob. The agent receives payload.message as the invocation message (or "Scheduled invocation" if no message is set).

See Build an AI Agent for details.


Permissions

Permission Grants
app:{appId}:cron.read List crons and view run history
app:{appId}:cron.write Create, update, delete crons
app:{appId}:cron.trigger Fire a cron manually
app:{appId}:cron.manage_others Manage crons created by other users

cron.read, cron.write, and cron.trigger are auto-generated when an app is installed (if no explicit permissions are declared). cron.manage_others must be declared explicitly or granted via wildcard.


Technical details

Property Value
Backend pg_cron extension
Requirement shared_preload_libraries = 'pg_cron' in postgresql.conf
Queue Same pgmq jobs queue as one-shot jobs
Overlap detection SQL check in rootcx_system.enqueue_cron(): queries pgmq.q_jobs for existing cron_id
Cron name constraints 1-64 chars, alphanumeric + dash + underscore
Response format camelCase (serde rename_all)

API endpoints summary

Method Path Permission Description
GET /api/v1/crons cron.read per app List crons across all accessible apps
GET /api/v1/apps/{appId}/crons app:{appId}:cron.read List crons for an app
POST /api/v1/apps/{appId}/crons app:{appId}:cron.write Create a cron
PATCH /api/v1/apps/{appId}/crons/{id} app:{appId}:cron.write Update a cron
DELETE /api/v1/apps/{appId}/crons/{id} app:{appId}:cron.write Delete a cron
GET /api/v1/apps/{appId}/crons/{id}/runs app:{appId}:cron.read List execution history
POST /api/v1/apps/{appId}/crons/{id}/trigger app:{appId}:cron.trigger Fire immediately