Scheduler and notifications
One scheduler runs reminders, to-dos, class reminders and scheduled checks. Each job pairs a trigger (once, or a series with an end) with an action (notify, check or agent), and fires on the minute.
Everything time-based in PKU harness is handled by one scheduler. Each item in the scheduler is called a job. A reminder, a to-do's reminder, a class reminder and "check my card balance every morning" are all jobs.
You can create jobs in the Schedule tab, or ask the agent to create them. Plugins create jobs
with services.scheduler.
Every job has two parts. The trigger decides when it runs, and the action decides what it does when it runs.
Anatomy of a job
type Job = {
id: string;
title: string;
trigger: Trigger; // when
action: Action; // what
enabled: boolean;
source: 'user' | 'agent' | 'plugin';
runs: number; // occurrences fired (or passed)
lastRunAt: number | null;
nextRunAt: number | null; // null when the series is over
// check / agent jobs also keep lastValue, lastError, history (last 20 runs), failures …
meta?: Record<string, unknown>; // link back to the owner, e.g. { todoId }
createdAt: number;
updatedAt: number;
};Triggers
There are two kinds of trigger. All times are in the phone's local time.
A one-time trigger, { type: 'at' }, has a single field, at, which is a timestamp in
milliseconds. It suits reminders such as "in 2 days" or "tomorrow at 8:00".
A repeating trigger, { type: 'every' }, suits reminders such as "every morning at 8",
"every 3 days, 16 times, then stop" or "Mon/Wed/Fri at 07:30 until the end of term". It has
these fields.
| Field | Meaning |
|---|---|
unit | The unit of repetition: minute, hour, day or week |
interval | Repeat every this many units, 1–1000 |
times? | Times of day as "HH:MM", for day and week |
weekdays? | Days of the week, 0 = Sunday to 6 = Saturday, for week |
startAt | When the series starts |
count? | The maximum number of runs |
until? | The last day of the series (inclusive) |
A repeating trigger is worked out as follows. A series is a sequence of periods. Each period is
one minute or hour step, one day, or one week starting on Monday. Each period contains slots,
formed by combining the times and weekdays. Slots before startAt are skipped. The remaining
slots are numbered 0, 1, 2… and cut off by count, until, or both.
The agent and forms use an input format that is easier to write, ScheduleInput, with times as
local "YYYY-MM-DD HH:mm" strings. For example, "every 3 days at 09:00, 16 times" is written as:
{ "type": "repeat", "unit": "day", "interval": 3, "times": ["09:00"], "count": 16 }toTrigger(input) converts this into a trigger. summarize(trigger) returns
{ schedule, next, total, finished }, for example "每 3 天 09:00 · 共 16 次" plus the next
three times. The agent can read these times back to you to confirm before it saves the job.
Actions
There are three kinds of action.
| Action | Fields |
|---|---|
notify | title, body?, url? |
check | tool, args?, condition, title, body?, url? |
agent | prompt (4–1000 characters) |
notify sends a notification. It is registered in advance as an exact notification, so no
code runs when it fires. url is the in-app route that opens when you tap the notification.
check is a scheduled check. It calls a read-only tool (in the form <plugin>.<tool>), reads
the value at condition.path in the result, and sends a notification only if the condition
holds. Any {value} in the notification text is replaced by the value it read.
agent runs the agent on a schedule. Each run starts a fresh, read-only agent session with
no chat history. If the agent replies NO_REPLY, there is nothing worth telling you. Otherwise,
its short answer becomes the notification.
A check condition is written { path, op, value? }. op is one of <, <=, >, >=, ==,
!=, changed and always.
This check reminds you to top up when your campus-card balance falls below ¥100:
{
"type": "check",
"tool": "wallet.cardBalance",
"condition": { "path": "balance", "op": "<", "value": 100 },
"title": "校园卡余额只剩 ¥{value},记得充值"
}On-time delivery on Android
notify jobs are registered in advance as exact alarms. Alarms are registered for the next
14 days, with at most 64 per job and 400 in total. They are registered again when the app starts,
whenever a job changes, when the app comes back to the foreground, and when the background task
runs.
check and agent jobs run in JavaScript. While the app is in the foreground, a timer runs
them on the minute. When the app is in the background or has been closed by the system, an exact
alarm wakes a background (headless) task. That task runs every job that is due, registers the
next alarm and removes the placeholder notification.
On-time delivery needs two permissions: the notification permission and the special "Alarms & reminders" access. Android 14 and later deny the second one by default. Turning off battery optimisation is recommended but not required.
Plugins can check and fix these settings with the following functions.
services.notify.readiness()reports the state of all three.deliveryBlocker(readiness)names the first thing that prevents on-time delivery.services.notify.fix(kind)opens the matching system prompt or settings screen. Callfixonly from a tap.
Limits on unattended runs
Nobody is watching when a scheduled check runs. For this reason, what it can do is limited, every run is logged, and failures are reported to you clearly.
| Rule | Value |
|---|---|
| Tools a scheduled run may call | read tools only |
| Enabled jobs | At most 50 (MAX_ACTIVE_JOBS) |
| Check frequency | At most every 15 minutes (MIN_CHECK_GAP_MS) |
| Number of agent jobs | At most 10 (MAX_AGENT_JOBS) |
| Agent job frequency | At most once an hour (MIN_AGENT_GAP_MS) |
| Steps per agent run | At most 8 (AGENT_MAX_STEPS) |
| Late runs | More than 2 hours late (MAX_LATE_MS) counts as missed |
| Run log | The last 20 runs (HISTORY_LIMIT) |
| Failures before a warning | 2 in a row (FAIL_ALERT_AFTER) |
| Failures before the job is turned off | 10 in a row (DISABLE_AFTER) |
Some of these rules need a few more words.
- The tool's effect is checked again at run time. If its mode is
ask, the call counts as declined. - Duplicate checks are not allowed, meaning checks with the same tool, arguments and condition.
- A late run counts as missed so that you don't get an out-of-date alert hours later.
- The run log records the planned and actual time, the result, the value read and any error.
- After 2 failures in a row, the app tells you once, and again after 6 hours if the job is still failing. After 10 failures in a row, the app turns the job off and tells you.
For plugin authors
- Make your tools easy to check. Such a tool has the
readeffect, responds quickly, and has a stable number or boolean field to compare, such asbalance,unreadoravailable. List these tools indataSourcesso that the agent can find them. - Create jobs only in response to something the user did, with
services.scheduler.create({ …, source: 'plugin' }). Keep the job id, inmetaor in yourkv, so that you can update or remove the job later. - The SDK exports the scheduler's pure calculation functions, such as
occurrencesAfter,nextOccurrence,describeTriggerandevaluate. See SDK → scheduler.
Dashboard widgets and data
There are two kinds of dashboard widget. The agent builds some from a generative-UI spec plus bindings to read tools, and plugins provide others as native components. The host handles refreshing, with one cache shared by all of them.
Campus sessions and IAAA
The harness keeps the campus credentials, signs in to each PKU service itself, and gives plugins the responses through `services.campus.request`. Plugins never see passwords or session tokens.