Schedules
Introduction
Section titled “Introduction”Use schedules when an agent should do something on a timetable without anyone opening the chat: a morning summary, a weekly report. A schedule is a recurring prompt. At every fire of its 5-field cron, read in an IANA time zone, its agent answers the prompt on a conversation of its own, as the schedule’s owner. Every instance ticks; the storage’s claim keeps one fire per due schedule.
Turning schedules on
Section titled “Turning schedules on”The routes are served by default. Two things decide who can use them: roles, and how a fire gets credentials.
import { PilotooModule } from '@pilotoo/server';
PilotooModule.forRoot({ scheduleTools: true, // schedule_create, schedule_list, schedule_update, schedule_delete, opt-in // A fire has no request: the host mints the credentials it runs with for the owner. scheduleCredentials: { credentialsFor: async (owner) => ({ authorization: `Bearer ${await mintTokenFor(owner.subject)}` }), },});SCHEDULE_ROLES=memberSCHEDULE_ADMIN_ROLES=adminSCHEDULE_ROLES lists the roles that may schedule runs as themselves; unset means nobody. SCHEDULE_ADMIN_ROLES lists the roles that may also read and change the tenant’s schedules. A SaaS server that already has its own login typically sets both and supplies a scheduleCredentials provider that mints a short-lived token for the owner.
Letting the agent schedule
Section titled “Letting the agent schedule”With scheduleTools: true an agent that names schedule_create can set one up from the conversation (“every weekday at 8, summarise my inbox”). The tools take the caller from the run, refuse a caller without a schedule role and act only on the caller’s own schedules. They refuse inside a scheduled run’s conversation, answer refusals as Not done: <reason>, and give a new schedule the conversation’s agent unless the call names another.
Creating one over HTTP
Section titled “Creating one over HTTP”POST /schedules{ "name": "Morning summary", "agentId": "assistant", "prompt": "Summarise yesterday.", "cron": "0 8 * * 1-5", "timezone": "Europe/Rome" }It answers 201 with upcoming, the next three fires, or 400 with the reason.
Showing schedules in your client
Section titled “Showing schedules in your client”createScheduleClient(apiUrl, fetch, { credentials }) from @pilotoo/chat-core calls these endpoints. SchedulesSection from @pilotoo/ui/settings is the settings section the demo web app mounts when canSchedule is true, with its strings in SettingsLabels.
<SchedulesSection client={schedules} agents={agents.agents} onOpenConversation={(threadId) => router.navigate(conversationHash(threadId))}/>A run’s conversation opens by id (thread:messages), which is how a client shows its transcript, tool calls and errors.
Options
Section titled “Options”| Setting | Default |
|---|---|
scheduleStorage |
PostgreSQL (tables created on first use; see Storage) when the module uses it. null without a database, and /schedules answers 501. |
scheduleCredentials |
None: a fire runs with no credentials, so the identity resolver must resolve empty credentials to the owner. |
SCHEDULE_ROLES |
Comma-separated roles that may schedule runs as themselves. Unset means nobody. |
SCHEDULE_ADMIN_ROLES |
Roles that may also read and change the tenant’s schedules (scope=tenant). Unset means nobody. |
SCHEDULE_MIN_INTERVAL_MINUTES |
5: no expression may fire sooner than this after its previous fire. |
SCHEDULE_MAX_PER_OWNER |
20. |
SCHEDULE_RUNS_KEPT |
20: older runs are deleted with their conversations. |
SCHEDULE_TICK_SECONDS |
30. 0 never fires, for a process that only serves the API. |
SCHEDULE_CLAIM_SECONDS |
300, at least 30 (a lower value stops the boot): how long a fire that never started holds its schedule before an instance retries it, and the lease of a running run. |
Endpoints
Section titled “Endpoints”| Endpoint | Answer |
|---|---|
GET schedules?scope=own|tenant |
{ schedules, canSchedule, canViewTenant }. A caller without a schedule role gets an empty list with canSchedule: false, so a client leaves the feature out. |
POST schedules |
{ name, agentId, prompt, cron, timezone?, enabled? }. 201 with upcoming, the next three fires; 400 with the reason. |
GET, PATCH, DELETE schedules/:id |
The caller’s own, or any of the tenant’s for an admin. Another owner’s answers 404. |
GET schedules/:id/runs |
Newest first: { id, threadId, status, error, startedAt, finishedAt, usage? }. usage is the run’s tokens from the usage store. |
POST schedules/:id/runs |
Fires it now, leaving its next fire as it is. 201 with the run as it starts. |
What the kit guarantees
Section titled “What the kit guarantees”- Who it runs as. A fire resolves its owner again, using the credentials
scheduleCredentialsgives for the owner.undefinedcredentials, or another identity, suspend the schedule. The roles are the resolved ones, never the stored ones.credentialsFor(owner, { request })says whether the call is inside the owner’s own request, so a host may mint from the owner’s bearer there and never from a fire or an admin’s request. Once an owner’s last schedule is deleted the kit calls the optionalrelease(owner), so the host may revoke what it holds. An owner without a schedule role, or whose agent is no longer active for them, does not run: the schedule is disabled withsuspendedReason, and enabling it again lifts the suspension. The kit never stores a token. - When. A missed fire runs once and moves to the next fire after now; there is no backfill. A run that fails never disables its schedule, and the next fire is the retry. A failure before the run starts skips this fire, and so does a previous run that is still running. The storage keeps at most one running run per schedule.
- Where it runs. Each fire is the first turn of a new conversation titled after the schedule, an ordinary run whose run id is the run row’s id, so its usage is under that
run_id. The conversation is hidden fromGET /conversationsand goes when the run is deleted. A run waiting onask_humanispausedand waits in that conversation. Deleting a schedule removes it and its runs, stops the running fires on this instance and deletes every run’s conversation. - Admins. A schedule admin changes, runs now and deletes any schedule of the tenant. A fire still runs as its owner, so the role lets its holder have an agent act as any owner in the tenant. Grant it accordingly.
- Restart. A running run holds a lease of
SCHEDULE_CLAIM_SECONDSthat the process renews every third of that. Every instance marksinterrupted, at boot and on each renewal, the running runs whose lease lapsed.
Next steps
Section titled “Next steps”- Usage to see what scheduled runs cost.
- Authenticating users for how a fire’s owner is resolved.
- Config tools for agents that manage their own configuration.