Molecule AI

Schedules

Run recurring prompts on cron schedules — automated audits, reports, and maintenance.

Overview

Schedules let you run recurring prompts against a workspace on a cron schedule. Each tick fires an A2A message/send into the workspace, so the agent processes the prompt as if it received a normal message. This enables automated audits, daily reports, weekly retrospectives, and any other recurring task.

The scheduler polls the workspace_schedules table every 30 seconds. When a schedule's next_run_at has passed, the scheduler fires the prompt and computes the next run time.

Scheduler (30s poll) ──> workspace_schedules table

                  next_run_at <= now?

                    ┌─────────┴──────────┐
                    │  A2A message/send   │──> Workspace Agent
                    │  (callerID=system:  │
                    │   scheduler)        │
                    └─────────────────────┘

Creating a Schedule

curl -X POST http://localhost:8080/workspaces/{id}/schedules \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {token}" \
  -d '{
    "name": "Daily Security Audit",
    "cron_expr": "0 9 * * *",
    "timezone": "America/New_York",
    "prompt": "Run a security audit of all open PRs. Check for leaked secrets, SQL injection, and auth bypass.",
    "enabled": true
  }'

Required fields:

FieldTypeDescription
cron_exprstringStandard cron expression (5-field: minute, hour, day-of-month, month, day-of-week)
promptstringThe text sent to the workspace as an A2A message each tick

Optional fields:

FieldTypeDefaultDescription
namestring""Human-readable label
timezonestring"UTC"IANA timezone for cron evaluation (e.g. America/New_York, Asia/Tokyo)
enabledbooltrueWhether the schedule fires

The timezone is validated against Go's time.LoadLocation on create and update. The cron expression is validated and the next run time is computed immediately.


CRUD Operations

MethodPathDescription
GET/workspaces/:id/schedulesList all schedules for a workspace
POST/workspaces/:id/schedulesCreate a new schedule
PATCH/workspaces/:id/schedules/:scheduleIdUpdate a schedule (partial update via COALESCE)
DELETE/workspaces/:id/schedules/:scheduleIdDelete a schedule

Update

PATCH accepts any subset of fields. Only provided fields are changed — the handler uses COALESCE in SQL so omitted fields retain their current values. If cron_expr or timezone changes, the next run time is recomputed.

curl -X PATCH http://localhost:8080/workspaces/{id}/schedules/{scheduleId} \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {token}" \
  -d '{"enabled": false}'

Delete

curl -X DELETE http://localhost:8080/workspaces/{id}/schedules/{scheduleId} \
  -H "Authorization: Bearer {token}"

All schedule operations are scoped to the owning workspace ID to prevent IDOR.


Manual Trigger

Fire a schedule immediately, outside its cron cadence:

curl -X POST http://localhost:8080/workspaces/{id}/schedules/{scheduleId}/run \
  -H "Authorization: Bearer {token}"

Returns the schedule's prompt so the frontend can POST it to /workspaces/:id/a2a. This keeps the handler stateless.


Run History

View the last 20 runs for a schedule, including error details for failed runs:

curl http://localhost:8080/workspaces/{id}/schedules/{scheduleId}/history \
  -H "Authorization: Bearer {token}"

Response:

[
  {
    "timestamp": "2026-04-16T09:00:02Z",
    "duration_ms": 4523,
    "status": "success",
    "error_detail": "",
    "request": {"schedule_id": "...", "prompt": "..."}
  },
  {
    "timestamp": "2026-04-15T09:00:01Z",
    "duration_ms": null,
    "status": "error",
    "error_detail": "A2A proxy returned 503: workspace container not running",
    "request": {"schedule_id": "...", "prompt": "..."}
  }
]

History is pulled from the activity_logs table filtered by activity_type = 'cron_run' and the schedule ID in the request body.


Source Field

Each schedule has a source field that tracks how it was created:

ValueMeaning
templateSeeded by an org template import or bundle import. On re-import, only template-source rows are refreshed — runtime rows survive.
runtimeCreated via the Canvas UI or API. These are user-owned and never overwritten by re-imports.

Status Values

The last_status field on a schedule tracks the outcome of the most recent run:

StatusMeaning
successThe A2A message was delivered and the workspace acknowledged it.
errorThe A2A proxy returned a non-2xx status. last_error contains details.
skippedThe workspace was busy (concurrency-aware skip). The scheduler detected active_tasks > 0 and deferred the run to avoid overloading the agent.

Schedule Health Endpoint

Peer workspaces can monitor each other's schedule health without admin auth:

curl http://localhost:8080/workspaces/{id}/schedules/health \
  -H "X-Workspace-ID: {callerWorkspaceId}" \
  -H "Authorization: Bearer {callerToken}"

This endpoint returns execution-state fields only (last_run_at, last_status, run_count, next_run_at, last_error). It deliberately omits prompt and cron_expr so sensitive task content is never exposed to peer workspaces.

Auth rules (mirrors the A2A proxy pattern):

  • X-Workspace-ID header required to identify the caller
  • Caller's own bearer token validated (legacy workspaces grandfathered)
  • registry.CanCommunicate(callerID, workspaceID) must return true
  • System callers (system:*, webhook:*, test:*) bypass checks
  • Self-calls always allowed

Scheduler Internals

Poll Loop

The scheduler runs a 30-second poll loop. Each tick:

  1. Queries up to 50 due schedules (next_run_at <= now AND enabled = true)
  2. Fires up to 10 concurrently via a semaphore
  3. Each fire sends an A2A message/send with a 5-minute timeout
  4. Updates last_run_at, run_count, last_status, and next_run_at
  5. Logs the run to activity_logs with activity_type = 'cron_run'

Panic Recovery

The scheduler recovers from panics inside the tick function. A single bad row, malformed cron expression, or database blip cannot permanently kill the scheduler. Without this recovery, the goroutine dies silently and the only signal is "no crons firing."

Liveness Watchdog

The scheduler reports heartbeats to the supervised subsystem. The /admin/liveness endpoint exposes per-subsystem ages, so operators can detect a stuck scheduler before it causes a missed-cron outage.

Scheduler.Healthy() returns true if the scheduler has completed a tick within the last 60 seconds (2x the poll interval). Returns false before the first tick or if the scheduler is stalled.


Examples

Hourly Security Audit

{
  "name": "Hourly Security Scan",
  "cron_expr": "0 * * * *",
  "timezone": "UTC",
  "prompt": "Scan all open PRs for leaked secrets, SQL injection patterns, and auth bypass vulnerabilities. Report findings as a summary."
}

Daily Standup Report

{
  "name": "Daily Standup",
  "cron_expr": "0 9 * * 1-5",
  "timezone": "America/Los_Angeles",
  "prompt": "Generate a standup report: what was completed yesterday, what is planned today, and any blockers. Post to the team channel."
}

Weekly Retrospective

{
  "name": "Weekly Retro",
  "cron_expr": "0 17 * * 5",
  "timezone": "America/New_York",
  "prompt": "Write a weekly retrospective covering PRs merged, issues closed, cron failures, and code review findings. Post as a GitHub issue."
}

Nightly Cleanup

{
  "name": "Nightly Cleanup",
  "cron_expr": "0 2 * * *",
  "timezone": "UTC",
  "prompt": "Archive stale branches older than 30 days. Close issues that have been inactive for 60 days with a comment explaining the auto-close policy.",
  "enabled": true
}

Timezone Handling

All cron expressions are evaluated in the specified timezone. If no timezone is provided, UTC is used. The timezone must be a valid IANA timezone string (e.g. America/New_York, Europe/London, Asia/Tokyo).

When a schedule's cron_expr or timezone is updated, the next_run_at is immediately recomputed using the new values. This prevents schedules from firing at unexpected times after a timezone change.


API Reference

MethodPathDescription
GET/workspaces/:id/schedulesList schedules
POST/workspaces/:id/schedulesCreate schedule
PATCH/workspaces/:id/schedules/:scheduleIdUpdate schedule
DELETE/workspaces/:id/schedules/:scheduleIdDelete schedule
POST/workspaces/:id/schedules/:scheduleId/runManual trigger
GET/workspaces/:id/schedules/:scheduleId/historyRun history (last 20)
GET/workspaces/:id/schedules/healthHealth view (open to peers)

On this page