Schedules
A schedule is a registered intention to fire a webhook at a future time,
optionally recurring. It is the simplest of the four primitives and the
foundation the others lean on: you hand us a moment (or a repeating rule) and a
webhook_url, and at that moment we POST to your endpoint. Nothing runs your
code in between — a schedule is a clock, not a job queue.
Reach for a schedule when the cue is the time itself: "call this agent back in three days", "post the digest every Monday at 09:00". Reach for a deadline instead when the thing you care about is an obligation with a completion state and escalation — "the certificate is due March 14, warn me a week ahead, and keep nagging if it lapses". A schedule fires and forgets; a deadline tracks whether the work got done. When in doubt: if you would ever want to mark it "done", it's a deadline.
How schedules behave
A schedule is either one-shot or recurring:
- One-shot — you give a single
fire_attimestamp (must be in the future). It fires once and is finished. - Recurring — you give a cron expression (
recurrence) and an IANA timezone (recurrence_tz, e.g.Europe/London). We evaluate the cron in that timezone, so a0 9 * * MONschedule stays at 9am local across daylight-saving shifts. Each occurrence is its own record; the ones sharing a rule form a series joined by arecurrence_series_id.
A recurring series advances by spawning its successor when an occurrence
resolves — whether it fired successfully or failed to deliver. The next
occurrence is computed from the moment it actually fired, so a backlog never
snowballs into a burst of catch-up callbacks. There is always exactly one
pending occurrence of a live series at a time.
Each schedule carries a status:
| Status | Meaning |
|---|---|
pending |
Registered, waiting for its fire_at. |
fired |
Its moment arrived and the webhook was dispatched. |
failed |
Its moment arrived but delivery ultimately failed. |
cancelled |
You cancelled it before it fired (see below). |
"Fired" means we dispatched the webhook, not that your endpoint acknowledged
it. Delivery has its own independent retry/backoff/dead-letter pipeline —
5xx/timeouts are retried with exponential backoff, 4xx are not. That
contract is owned by Webhooks & signing;
a recurring series keeps advancing regardless of any single delivery's fate.
Create a schedule
Every create is idempotent: pass an idempotency key and a retried create returns the original schedule instead of making a second one. See Idempotency for how to choose keys.
Go to Schedules → New schedule. Choose One-shot and pick a date and
time, or choose Recurring and enter a cron expression plus a timezone. The
form shows a live cron preview — the next three fire times in your chosen
timezone — so you can confirm the rule reads the way you meant before saving.
Paste the webhook_url the callback should POST to.
POST /v1/schedules/ with your API key as a Bearer token. A one-shot needs
fire_at; a recurring one needs recurrence + recurrence_tz (the server
computes the first fire_at for you). Pass an Idempotency-Key header to make
retries safe.
# One-shot: fire once, three days out
curl https://aigears.example.com/v1/schedules/ \
-H "Authorization: Bearer aigears_live_..." \
-H "Idempotency-Key: call-me-back-42" \
-H "Content-Type: application/json" \
-d '{
"fire_at": "2026-07-06T09:00:00Z",
"webhook_url": "https://hooks.example.com/aigears",
"payload": {"task_id": "42"}
}'
# Recurring: every Monday at 09:00 London time
curl https://aigears.example.com/v1/schedules/ \
-H "Authorization: Bearer aigears_live_..." \
-H "Content-Type: application/json" \
-d '{
"recurrence": "0 9 * * MON",
"recurrence_tz": "Europe/London",
"webhook_url": "https://hooks.example.com/aigears"
}'
The payload you supply is echoed back to you inside the fired webhook, so use
it to carry whatever context your handler needs to act. Before committing to a
cron rule you can dry-run it with POST /v1/schedules/cron-preview/
({"cron": ..., "tz": ...}) — it returns the next occurrences and creates
nothing, costs nothing, and touches no quota.
Ask the agent in plain language; it translates the phrasing into a
schedule_create call.
"Call me back in three days to check on the migration."
→
schedule_create(fire_at="2026-07-06T09:00:00Z", webhook_url="https://hooks.example.com/aigears", payload={"about": "migration"})"Post the weekly report every Monday at 9am, London time."
→
schedule_create(recurrence="0 9 * * MON", recurrence_tz="Europe/London", webhook_url="https://hooks.example.com/aigears")
schedule_create takes the same fields as the HTTP body — fire_at for
one-shot, recurrence + recurrence_tz for recurring, plus webhook_url,
payload, and an optional idempotency_key. Invalid cron or an unknown
timezone comes back as a tool error the agent can read and correct.
Inspect schedules
Listing shows one row per series (the live occurrence stands in for its rule), while fetching a single schedule gives you its full state plus its delivery history — every attempt we made to reach your endpoint, with status codes.
The Schedules list shows each series as a single row with its next fire time and status. Open a row for the detail page: the rule, the upcoming occurrence, and the delivery log for what has already fired.
GET /v1/schedules/ lists your team's schedules. Narrow it with
?status=pending (or any status value) and ?series_id=<uuid> to walk a single
recurring series. GET /v1/schedules/{id}/ returns one schedule with its nested
deliveries array — the attempt-by-attempt delivery history.
curl "https://aigears.example.com/v1/schedules/?status=pending" \
-H "Authorization: Bearer aigears_live_..."
schedule_list returns the team's schedules; schedule_get(schedule_id=...)
returns one with its delivery history.
"Did my Monday report schedule fire last week?"
→
schedule_list(), thenschedule_get(schedule_id="...")to read the delivery log on the relevant occurrence.
Cancel a schedule
You can only cancel a schedule while it is pending. Cancelling one that has
already fired, failed, or been cancelled returns a 409 — there is nothing left
to stop.
There is one rule worth internalising for recurring schedules. A live series is
represented by its single pending occurrence, and cancelling that occurrence
terminates the series — no successor is spawned, so the whole recurrence
stops. There is no separate "delete the series" action; cancel the pending
occurrence and the rule is done.
Use the Cancel action on a schedule's row or its detail page. For a recurring schedule this stops the entire series, not just the next fire.
POST /v1/schedules/{id}/cancel/. It is idempotent — pass an Idempotency-Key
so a retried cancel is a no-op rather than an error.
curl -X POST https://aigears.example.com/v1/schedules/01J9Z8XKQR.../cancel/ \
-H "Authorization: Bearer aigears_live_..."
schedule_cancel(schedule_id=...).
"Stop the weekly report — we don't need it any more."
→
schedule_cancel(schedule_id="..."), which ends the whole series.
The webhook you receive
When a schedule fires, we POST a schedule.fired event to its webhook_url.
Inside the standard signed envelope, the data payload is small — an id and the
payload you registered:
{
"event_type": "schedule.fired",
"data": {
"schedule_id": "01J9Z8XKQR...",
"payload": { "task_id": "42" }
}
}
The envelope shape, the X-AIGears-Signature header, and the retry/dead-letter
rules are the same for every primitive and are documented once in
Webhooks & signing. For the how-to of
standing up a receiver and verifying the signature, see
Receiving webhooks. Your handler must be
idempotent on event_id, because delivery is at-least-once.
Limits
Creating a schedule counts against your plan's flow limit (the number of callbacks you can register per month). Recurring occurrences that we spawn automatically do not cost extra — only the create you initiate is metered, so a weekly schedule costs one, not fifty-two. Reads and inspections are never metered. The per-tier numbers, and what happens when you reach a limit (free tiers are capped hard; paid tiers get a soft grace window), live in Limits & tiers.