Skip to content

Scheduling

A schedule starts an agent on a five-field cron expression, in an IANA timezone, with its own input template. One agent can have many schedules, and a scheduled run looks like any other run in the run list and journal. An agent that is over its hard-fail budget does not fire, so an unattended schedule cannot run past its cap.

Cron schedules and timezonesA schedule resolves a timezone, computes its next fire time, and the scheduler claims and fires it when due.
POST /v1/schedulescronExpr · timezone?Schedule timezoneif setPAL_DEFAULT_TIMEZONEdeployment defaultUTClast resortResolveLocationfirst match winsNextCronTimein that zoneschedulesnext_fire_atScheduler pollevery 60 sClaim due rowsFOR UPDATE SKIP LOCKEDBudget checkhard-fail capsRunshows in the console
Swipe sideways to pan.

1. Created with a zone

The request carries a timezone such as America/New_York next to the cron expression.

  • API call
  • Control plane
  • Data stores

Five fields: minute, hour, day of month, month, day of week. Each field accepts *, lists, ranges and */N steps.

# ┌─── minute (0-59)
# │ ┌─── hour (0-23)
# │ │ ┌─── day of month (1-31)
# │ │ │ ┌─── month (1-12)
# │ │ │ │ ┌─── day of week (0-7, Sun=0 or 7)
# │ │ │ │ │
# * * * * *
0 9 * * 1-5 # every weekday at 9:00 AM (in the schedule timezone)
*/15 * * * * # every 15 minutes
0 0 1 * * # first day of every month at midnight
Terminal window
curl -s -X POST http://localhost:8080/v1/schedules \
-H "Authorization: Bearer $PAL_API_KEY" -H "Content-Type: application/json" -d '{
"agentName": "research-agent",
"cronExpr": "0 9 * * 1-5",
"timezone": "America/New_York",
"inputTemplate": { "mode": "daily-digest" },
"deliveryEmail": "me@example.com",
"enabled": true
}'

The response is 201 Created:

{
"id": "sched-uuid",
"tenantId": "...",
"agentName": "research-agent",
"cronExpr": "0 9 * * 1-5",
"inputTemplate": { "mode": "daily-digest" },
"deliveryEmail": "me@example.com",
"enabled": true,
"nextFireAt": "2026-07-28T13:00:00Z"
}

deliveryEmail is optional. The create response does not echo timezone, but nextFireAt is already computed in that zone.

Action Request Response
List GET /v1/schedules 200, a bare array of schedules
Update PUT /v1/schedules/{id} with e.g. { "enabled": false } 200 with a JSON body that includes id and cronExpr
Delete DELETE /v1/schedules/{id} 204 No Content

PUT accepts enabled, cronExpr, deliveryEmail and timezone only. To change inputTemplate, delete the schedule and create a new one.

timezone is an optional IANA name; an invalid one returns 400. Cron then matches the schedule’s local time. Resolution order: the schedule’s zone, then PAL_DEFAULT_TIMEZONE, then UTC. With neither set, 0 9 * * * fires at 09:00 UTC.

The deployment default (not a per-schedule zone) also sets the budget day boundary, so daily limits roll at local midnight in that zone.

examples/daily-digest is a workflow (GitHub open PRs, an AI step, a Slack post) on the schedule 0 9 * * 1-5 in America/New_York.

  1. Run pal deploy in that directory. The manifest carries the prompt and workflow.
  2. Install the Slack and GitHub connectors (Connectors).
  3. Post schedule.json to POST /v1/schedules.

The response’s nextFireAt is the next weekday 09:00 New York time.

To test without waiting for 09:00, set the row due (UPDATE schedules SET next_fire_at = now() WHERE id = ...) and watch GET /v1/runs?agent=.... The e2e suite does this against fixtures (Testing).

  • Polling: the scheduler polls every 60 seconds, so a due schedule fires within a minute.
  • Replicas and downtime: with several control-plane replicas each due window fires at most once. If the process is down at fire time, that occurrence is skipped, not replayed.
  • REST only. There is no pal schedule command and no SDK namespace for cron schedules.
  • deliveryEmail also mails the run output. The fixture suite does not cover that path.

Inbound channel messages (Set up PAL) and webhook triggers start runs too, with the same lifecycle.