Run work on a schedule while your app's window is closed — sync, poll, a daily digest. Capability: background — disclosed at install with each task's schedule, prompts once on first fire.
Declare the tasks in "backgroundTasks" (this list is the ceiling; app code can't invent a new schedule), then register ONE handler at load. ChatOSS fires a due task by mounting your app headless (offscreen, no UI), calling your handler with the task id, and disposing it when the handler resolves:
// app.json: {
// "capabilities": ["background", "hostHttp"],
// "httpAllowlist": ["api.example.com"],
// "backgroundTasks": [{ "id": "sync", "name": "Sync inbox", "trigger": { "type": "interval", "minutes": 30 } }]
// }
window.chatoss.background.onTask(async (taskId) => {
if (taskId === 'sync') {
const res = await window.chatoss.http.request({ url: 'https://api.example.com/inbox' });
await window.chatoss.scopedData.set('inbox', JSON.parse(res.body)); // persists for the next window open
}
});
Triggers
{ type: "interval", minutes } (floored to 5) · { type: "daily", hour, minute } · { type: "weekly", weekday, hour, minute } (0 = Sunday) · { type: "manual" } (runs only when the user hits Run now in the app's Activity view). Max 8 tasks.
Keep runs SHORT and idempotent
There's a per-run time budget (~30–60s) — over it, the run is force-disposed. One run at a time per task; a slow run doesn't stack.
Your WHOLE app boots for each headless run
index.html runs top to bottom with no window. Gate load-time side effects (auto-running checks, sounds, notification sends) with:
const headless = await window.chatoss.background.isHeadlessRun();
// a headless boot should register onTask and nothing else.
What works headless (the working set)
scopedData, data (incl. publish), scopedTools, http, chat.runTurn, web, notifications, clipboard.writeText, db, drive (list/stat/read/write/mkdir/move/copy/remove/usage), boards, documents.generate, secrets, mcp, process, apps (call + registerApi + listApis), platform, approvals, manifest, proposeTask, and terminal — including exec/spawn/spawnCodingAgent and every session method, but ONLY for command prefixes you declared in terminalCommandPrefixes (an undeclared prefix is auto-denied: there is no window to prompt in). files read/write DOES work headless — readFile, writeFile, listDir, search, watch all run, provided a root was already picked in an earlier visible run (picked roots are persisted); with no picked roots they reject.
What refuses / no-ops headless (no window to use)
Every webview call, the files dialogs (openDialog, saveDialog, pickFolder) and files.onDrop, documents.save (it opens a save dialog), drive.exportTo/drive.importFrom (they touch the user's real disk), shortcuts.register, clipboard.readText, openExternal.open, terminal.requestSession, global tools.register/requestRegister, appInstall.install, and preview.launch.
Store results in scopedData/db/drive and render them the next time the window opens.
Honest scheduling
Tasks fire only while ChatOSS itself is running (the window being closed is fine; a full quit — ⌘Q / Ctrl+Q — is not). A schedule missed while ChatOSS was quit fires on the next launch. This is a desktop app, not a server.
Consent
"background" is disclosed on the install screen (with each task's schedule) and prompts once on first fire (Allow once / always / Deny; a Trusted app skips the prompt). The user can switch it off any time in Permissions — that stops all the app's tasks immediately.
Users can watch and trigger tasks too
Apps → Background activity lists every declared task's schedule, live status, and run history, with a Run now button — design handlers to be safe to run twice, since a manual fire can land right next to (or instead of) a scheduled one.