Skip to content
Pilotoo
v0.1

Schedules

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.

The routes are served by default. Two things decide who can use them: roles, and how a fire gets credentials.

app.module.ts
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)}` }),
},
});
.env
SCHEDULE_ROLES=member
SCHEDULE_ADMIN_ROLES=admin

SCHEDULE_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.

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.

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.

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.

apps/web/src/features/settings/settings-screen.tsx
<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.

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.
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.
  • Who it runs as. A fire resolves its owner again, using the credentials scheduleCredentials gives for the owner. undefined credentials, 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 optional release(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 with suspendedReason, 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 from GET /conversations and goes when the run is deleted. A run waiting on ask_human is paused and 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_SECONDS that the process renews every third of that. Every instance marks interrupted, at boot and on each renewal, the running runs whose lease lapsed.