How to Configure Scheduled Work
GSV has two scheduling surfaces:
- Kernel schedules through
sched.*for user/process-owned work. - Package daemon schedules for package-owned backend RPC methods.
Use Kernel schedules when scheduled work should create or notify a process. Use package daemon schedules when a package backend needs to call one of its own RPC methods.
Add a Kernel Schedule
Kernel schedules have an expression and a typed target.
From a GSV shell:
sched add --name "daily ops check" --cron "0 9 * * *" --timezone Europe/Amsterdam \
--profile cron --label "daily ops check" \
"Check system health and summarize anything that needs attention."Programmatically:
await kernel.request("sched.add", {
name: "daily ops check",
expression: {
kind: "cron",
expr: "0 9 * * *",
timezone: "Europe/Amsterdam",
},
target: {
kind: "process.spawn",
profile: "cron",
label: "daily ops check",
prompt: "Check system health and summarize anything that needs attention.",
},
});Supported expression shapes:
{ kind: "at", atMs: Date.now() + 60_000 }
{ kind: "after", afterMs: 60_000 }
{ kind: "every", everyMs: 3_600_000, anchorMs: Date.now() }
{ kind: "cron", expr: "0 9 * * *", timezone: "Europe/Amsterdam" }Cron expressions use five Linux-style fields:
minute hour day-of-month month day-of-weekThe timezone must be an IANA timezone. If the system was initialized through onboarding, the selected system timezone is available as config/server/timezone.
Notify an Existing Process
Use process.event when the schedule should wake an existing process conversation instead of spawning a new process.
From a GSV shell:
sched add --name "ops pulse" --every 15m --pid "$GSV_PID" --conversation ops \
--message "Run the scheduled ops pulse."Inside a process shell, $GSV_PID and proc self both identify the current process.
Programmatically:
await kernel.request("sched.add", {
name: "ops pulse",
expression: { kind: "every", everyMs: 15 * 60 * 1000 },
target: {
kind: "process.event",
pid: "init:1000",
conversationId: "ops",
message: "Run the scheduled ops pulse.",
data: { source: "cron" },
},
});The target process sees this as a runtime event. In model context it is rendered with the normal [Process Event]: prefix.
Manage Kernel Schedules
await kernel.request("sched.list", { includeDisabled: true });
await kernel.request("sched.update", {
id: "schedule-id",
patch: { enabled: false },
});
await kernel.request("sched.remove", { id: "schedule-id" });To run a schedule manually:
await kernel.request("sched.run", { id: "schedule-id", mode: "force" });To sweep currently due schedules:
await kernel.request("sched.run", { mode: "due" });Add a Package Daemon Schedule
Package backends can schedule their own RPC methods through this.daemon. Schedules live in the package AppRunner Durable Object, not in the Kernel scheduler table.
import { PackageBackendEntrypoint } from "@gsv/package/backend";
export default class ReportsBackend extends PackageBackendEntrypoint {
async enableDailyReport() {
if (!this.daemon) {
throw new Error("daemon scheduling is unavailable");
}
return this.daemon.upsertRpcSchedule({
key: "daily-report",
rpcMethod: "runDailyReport",
schedule: { kind: "every", everyMs: 24 * 60 * 60 * 1000 },
payload: { channel: "ops" },
enabled: true,
});
}
async runDailyReport(payload: { channel?: string }) {
const files = await this.kernel.request("fs.search", {
path: "/workspaces",
query: "TODO",
});
await this.storage?.sql.exec(
"INSERT INTO report_runs(created_at, payload_json) VALUES (?, ?)",
Date.now(),
JSON.stringify({ payload, files }),
);
return { ok: true };
}
}