A job is a stored schedule that starts a script on the desktop, or sends a message to the chat. It is not a timer in an app view. Redac must be running for execution. A phone or closed app view does not execute the job itself.
Normal imported app: manifest schedule
Declare the script, its input/result schemas, and this item in manifest.schedules:
{
"name": "daily-count",
"script": "count-tasks",
"cron": "0 9 * * *",
"timeZone": "local",
"input": {}
}
This is a fragment, used in the complete Task Counter manifest. The script name must match a declared script and input must match its schema. Manifest schedules currently target scripts only. A script can send a message to its app's agent with agent.send; that turn is a child of the run. Only the owner can make a schedule that sends a message to the chat, in Schedules.
New manifest schedules start paused. The owner opens Schedules, reviews the app and target, and enables the schedule. Importing an app does not enable its jobs. A view or Chat can create a script schedule with schedules.manage on the target app resource. An app with a manifest must also declare the target in uses, else Access shows no row for it. Other apps start Off. A schedule on another app runs only when the creator has Allow on both scripts:<x> scripts.run and schedules:<x> schedules.manage. Thus, declare both. Cross-app schedules remain owned by the target app and record the initiating creator. A creator can manage only its own schedules; the target app and owner shell can manage all of them.
local is resolved to a concrete desktop time zone when the schedule is installed or normalized. Use an IANA zone such as America/Los_Angeles when the app needs a fixed zone. Changing the machine zone later is not a request to rewrite an existing stored zone.
Time rules
App entries (schedules.*, scheduleAdmin.*) and manifest schedules keep
cron as input. Only these numeric five-field forms are accepted:
minute hour day-of-month month day-of-week.
| Accepted form | Rule |
|---|---|
M H * * * | Daily at H:M |
M H * * D | Weekly at H:M; D is one weekday, a list such as 1,3,5, or a range such as 1-5, using 0–6 |
*/n * * * * | Every n minutes; n must divide 60 (* * * * * is n = 1) |
M * * * * | Every hour, at minute M |
M */n * * * | Every n hours, at minute M; n must divide 24 |
M is a whole minute from 0 to 59. H is a whole hour from 0 to 23. Sunday
is 0, not 7. For example, 0 9 * * * is Daily at 9:00 AM,
0 9 * * 1-5 is Mon–Fri at 9:00 AM, and */15 * * * * is Every
15 minutes. Lists and ranges apply only to the weekday field in the Weekly
form. Steps apply only to the minute and hour Interval forms in the table.
There is no seconds field, named month/day, or @daily alias.
All other lines are refused on create/update with
invalid_args: "Use Daily, Weekly or Interval.". A manifest schedule with
a line that does not fit installs paused and shows Needs a new time.
The owner must save a new time before enabling it.
Times use the schedule's local clock. A nonexistent daylight-saving time is skipped. Minute and hour intervals run at both instants of a repeated local time. Daily and Weekly schedules run only at the first instant.
Runtime behavior
| Event | Behavior |
|---|---|
| Deadline missed while desktop is stopped/asleep | Past occurrences are skipped; there is no backlog replay |
| Prior occurrence still active or awaiting approval | New overlapping occurrence is skipped |
| Pause, edit, removal or owner shutdown during admission | Admission guard prevents a new effect |
| Run already accepted before pause/remove | Keep the accepted run link; cancellation is a separate operation |
| Restart with a pending occurrence | Look up its durable receipt; link a known run or mark interrupted; do not blindly start again |
| App disabled, target missing or revision stale | Schedule becomes unavailable or is paused by its lifecycle |
| Schedule removed | Retain occurrence history |
| App removed | Delete its schedules, the schedules it created, and their occurrences |
Occurrences store schedule version, definition revision, target metadata, time rule, request key, and accepted run ID. An app revision change pauses schedules and can retire old manifest targets. Runtime Access applies to scheduled execution, including AI and tools. An approval wait counts as active work; scheduled jobs do not bypass owner approval.
The host allows up to 64 schedules per app, including user-created schedules; the manifest itself allows 16 seeds. Results are available in Activity and Schedules run details. Saving a report record or showing a notification requires the script to implement that effect through a supported capability.
Shared schedule capabilities
Declare scripts.run and schedules.manage. Set Access on scripts:task-counter and schedules:task-counter. Automatic delegated runs need both grants at Allow and the creator app enabled. The scheduler checks them before each run; Ask cannot authorize an unattended start. The target app still needs its own effect grants.
import type { AppClient } from '@redac/sdk';
export async function createCountSchedule(client: AppClient) {
await client.ready;
const { apps } = await client.cap.call('scripts.catalog', {
appId: 'task-counter',
});
const app = apps.find((item) => item.id === 'task-counter');
if (!app || app.state !== 'ready') throw new Error('Task Counter is unavailable.');
const schedule = await client.cap.call('schedules.create', {
appId: app.id,
definitionRevision: app.revision,
name: 'Daily count',
cron: '0 9 * * *',
timeZone: 'America/Los_Angeles',
target: { kind: 'script', script: 'count-tasks', input: {} },
requestKey: crypto.randomUUID(),
});
// Present this paused schedule to the owner before enabling it.
return schedule;
}
export async function enableSchedule(client: AppClient, id: string, currentVersion: number) {
return client.cap.call('schedules.setEnabled', {
appId: 'task-counter',
id,
expectedVersion: currentVersion,
enabled: true,
});
}
Keep the create request key and payload if its reply is lost. Edits and enable changes use the current stored version; stale saves are refused. Reload after a stale version, preserve the user's draft, and do not guess a new version.
| API | Arguments and purpose |
|---|---|
scheduleAdmin.targets | Optional app/cursor/limit (1–20); get live app revision, scripts and profiles |
scheduleAdmin.models | appId; get the offered models of the app |
scheduleAdmin.create | App/revision, name, cron, zone, target and UUID key; returns paused summary |
scheduleAdmin.update | ID, expected version, definition revision and nonempty changes; returns summary |
scheduleAdmin.setEnabled | ID, expected version and enabled flag; returns summary |
scheduleAdmin.remove | ID and expected version; retains history |
scheduleAdmin.list / watch | Optional app/cursor/limit/includeRemoved; page or stream summaries |
scheduleAdmin.get | ID and optional history before/limit; summary, full target and occurrences |
scheduleAdmin.getRun | Schedule ID and occurrence run ID; typed script result |
scheduleRules.create | App/revision, name, repeat, zone, target and UUID key; no cron; returns paused summary |
scheduleRules.update | ID, expected version, definition revision and nonempty changes; optional repeat, no cron |
scheduleOwner.* | Other owner operations also support chat targets; models gives Chat's models |
scheduleRules.create requires repeat. scheduleRules.update takes an
optional repeat in changes. Neither takes cron. The ScheduleRepeat
shape is:
type ScheduleRepeat =
| { kind: 'daily'; at: string }
| { kind: 'weekly'; days: number[]; at: string }
| {
kind: 'interval';
every: number;
unit: 'minutes' | 'hours' | 'days' | 'weeks' | 'months';
first: string;
};
at is HH:MM in 24-hour time. days has 1–7 unique weekdays, sorted
from 0 (Sun) to 6 (Sat). first is YYYY-MM-DDTHH:MM in the schedule's
zone. every is a whole number: 1–1440 minutes, 1–720 hours, 1–365 days,
1–52 weeks, or 1–12 months. App entries and manifests do not take repeat.
Only owner administration can create a chat schedule, with scheduleRules.create. Its app is chat (listed by scheduleOwner.targets) and its target is { kind: 'chat', message, model }. Select a model ref from scheduleOwner.models. Each occurrence opens a new chat with the schedule name and the time as its title; its run is the turn of that chat (scheduleOwner.getRun gives { kind: 'chat', runId, chatId, state, error }). A failed turn makes the occurrence failed. The scheduleAdmin.* and schedules.* entries leave chat schedules out and give not_found for their IDs. The host refuses the old agent (harness) target that their schemas keep.
The owner shell and bundled Schedules view use scheduleRules.* for rule reads and writes, and the unchanged scheduleOwner.* operations for other administration, including chat schedules. Shared schedules.* calls create script targets only.
Owner rule entries and old clients
The Desktop UI uses owner-only scheduleRules.create, scheduleRules.update,
scheduleRules.list, scheduleRules.watch, scheduleRules.get and
scheduleRules.setEnabled. These entries use repeat, including nullable
repeat and optional cron in result summaries and occurrences. A schedule
that needs a new time has reason needs_time.
All existing entries keep their shapes. schedules.*, scheduleAdmin.* and
scheduleOwner.* still take cron and return required cron, with no
repeat. When a rule has no cron form, that result field holds canonical
rule text. Apps must treat non-cron text in cron as opaque: do not parse
or change it. Send it back unchanged on update to keep the saved rule.
Only Redac's Schedules screen reads rules through the owner-only
scheduleRules.* entries. For old clients,
a needs-time row has available: false and reason definition_changed.
The contract version is 18 and its minimum stays 7.