Skip to main content
Run a tool on a schedule through the backend. The runtime runs a registered tool periodically. A schedule names the tool, its arguments, and a cadence; a worker backend fires it on time. Scheduled runs execute on the backend, so a backend plugin must be configured for schedules to run.

Configure a backend

A schedule runs on the worker backend, not in the request path. Register a backend plugin under backend_module in the manifest and start its worker runtime:
manifest.yml
A registered backend needs the worker bus and a shared hooks registry: set TAI_BUS_REDIS_URL and HOOKS_REDIS_URL (or TAI_DEFAULT_REDIS_URL for both), or tai serve and tai backend refuse to start. See Backends for where tools run.

Attach the schedule extension

A schedule on a cadence fires the tool’s <tool>_schedule_task vehicle — the backend’s schedule_task extension. You always name the base tool; the server resolves the vehicle. Naming the backend under backend_module registers that extension, but it is never attached to a tool on its own: name it in the tool’s per-tool extensions map so the vehicle exists.
manifest.yml
Without the extension the tool has no schedule vehicle, and tai schedules add report --cron ... answers a 404 — the cadence has nothing to run on. GET /api/tools/tags marks each tool schedulable when its vehicle is registered, which is how Studio’s schedule dialog picks the tools it offers.

Add a schedule

Name the base tool and its cadence. Pass tool arguments with --tool-kw key=value (or --tool-kwargs as JSON), and give the cadence with at most one of:
  • --cron EXPR — a crontab expression: 5 fields (minute hour day-of-month month day-of-week), or 6 with a leading seconds field, which is dropped.
  • --every SECONDS — an interval, in seconds (greater than 0).
  • --cadence JSON — the cadence object itself (below).
With no cadence option the tool runs once, now. --schedule-name names a recurring schedule (its handle for tai schedules delete); it needs a cadence. Without it the server derives a name from the tool, its arguments and the cadence, so re-adding the same schedule updates it in place — pass --schedule-name when you want an explicit, stable handle.

The cadence

Every door speaks one cadence shape, discriminated by kind:
relative aligns an interval to its own clock. The Celery backend honours it; the arq and RQ backends cannot, and refuse a schedule with relative: true rather than ignoring it. Over HTTP, POST /api/schedules takes the same fields:
The body is closed: an unknown key is a 400, and so is naming the <tool>_schedule_task vehicle as tool_name or giving schedule_name without a cadence. Every door checks what the schedule names. An unknown tool is a 400 (scheduled tool 'ghost' is not a registered tool), and the tool_kwargs are checked the way a preset body is, for every kind of target: an agent’s tool_names entry, a stored template id at a templated field, or a name a base tool’s tool_refs extractor reports. Each miss is one line of the 400, for example tool_kwargs.tool_names entry 'ghost' is not a registered tool.

Driving parked interactions

A schedule can also be a parkable-driving door: on each fire it can cancel, resume, take, or start runs against the parked interactions on its subject. Pass the four door-contract jqs — each evaluated over the schedule’s fired tool arguments with the run’s parked interactions bound as $parked:
  • --start-expr builds the fired tool’s kwargs (null starts nothing).
  • --cancel-expr names parked interaction ids to cancel.
  • --resume-expr names an ask to resume with an answer ({id, payload}) or a waiting outcome to take (a bare id).
  • --extras-expr builds the extras mapping handed to the started target.
Any of the four requires --execution-key — the identity the fired run runs as. A schedule fire has no receiver, so a resumed run’s terminal waits on the subject for its next owner to take.
A schedule created with a state_binding has the binding checked and its templates attached at create, before the backend stores anything: a binding naming an undeclared state is a 404, an occupied attach path a 409, and a states-store failure a 500. See saving a binding.

Inspect and remove

1

List schedules

Each row names the schedule, the tool it fires, its cadence, whether it is enabled, and its next run — never the stored tool arguments.
2

Check the server clock

Cron cadences resolve against the server’s clock — read it to reason about when a job fires.
3

Delete a schedule

A schedule the worker cannot recover — one that keeps failing when the watchdog tries to read or re-arm it — carries an error (its message and when it happened) in the listing rather than being retried silently forever, so tai schedules list tells you which schedule needs attention. A scheduled run, like every dispatched job, is bounded by the backend’s job budget, <PREFIX>_TASK_TIMEOUT (see Backends).
A schedule and a webhook binding are the two ways a tool fires without a client call. To fire a tool on an inbound event instead, see Fire a tool from a webhook.

Manage schedules in the Studio

The Studio’s Scheduling page lists the deployment’s schedules: each row names the schedule, its cadence (Every 3600s, or Cron: and its five fields), whether it is enabled, and the tool it fires. A schedule carrying an error shows an Error badge beside its status; the badge’s tooltip carries the message, and the time it happened when the backend recorded one. The page also shows the server clock that cron cadences resolve against.
The schedules table with columns Name, Schedule, Status, Tool and Actions: hourly-sync every 3600 seconds, nightly-refresh and weekly-digest on cron cadences, weekly-digest with an Error badge.The schedules table with columns Name, Schedule, Status, Tool and Actions: hourly-sync every 3600 seconds, nightly-refresh and weekly-digest on cron cadences, weekly-digest with an Error badge.

The Scheduling page: an interval row, two cron rows, and a schedule with an Error badge.

Add schedule opens the create dialog: a name, the tool (only tools the server marks schedulable are offered), its kwargs as JSON, and an Interval or Crontab cadence. Below the cadence sit the schedule’s subject, the execution key the fire runs as (shown only with access control on; with it off the schedule runs as the gate-off identity, and the field is absent; a key session is offered its own identity when the list does not hold it, an account session must mint a key first, and a session that may not list api keys is told which read it needs), and the Start, Cancel, Resume and Extras jqs of driving parked interactions, and the schedule’s state binding.
The Add schedule dialog with name nightly-refresh, the tool studio_demo_echo, an empty kwargs object, Crontab selected with 0 2 * * *, and an execution key chosen.The Add schedule dialog with name nightly-refresh, the tool studio_demo_echo, an empty kwargs object, Crontab selected with 0 2 * * *, and an execution key chosen.

The Add schedule dialog on a crontab cadence, with the execution key the fire runs as.

A cron expression of the wrong length is refused in the field, before anything is sent: it takes 5 fields, or 6 with a leading seconds field.
The Cron expression field holding 0 2 * * in its error state, with the message: Use 5 fields (minute hour day-of-month month day-of-week), or 6 with a leading seconds field.The Cron expression field holding 0 2 * * in its error state, with the message: Use 5 fields (minute hour day-of-month month day-of-week), or 6 with a leading seconds field.

A four-field cron expression refused in the dialog.

See also