Skip to content
GurtOS

Schedule a routine

A routine is a task the box starts on its own, on a timer. In this guide you write a small app with a schedule trigger, install it on the box, and check that it starts tasks. The schedule re-arms every time the box starts, so it survives restarts and power cuts.

  • A box installed on Ubuntu from a source checkout. See Install on Ubuntu. A box installed from an ISO has only the apps in its image, and a Server box has no shell to install one from, so use an Ubuntu install for your own app.
  • A model key in the vault (ANTHROPIC_API_KEY or FACTORY_API_KEY), because scheduled tasks run Droid with Claude by default. See Add a secret.
  • Basic TypeScript.
  1. Make the app's folder in your checkout. The folder name is the app's name.

    mkdir -p tracks/nightly
  2. Write tracks/nightly/track.ts. This app starts one task every day at 02:00, box local time.

    import type { Track } from '../../core/contract.ts';
    import { schedule } from '../../core/triggers.ts';
    export default {
    name: 'nightly',
    triggers: [
    schedule({
    name: 'report',
    dailyAt: '02:00',
    task: () => ({
    title: 'Nightly notes report',
    prompt: 'Read notes/ and write report.md: what changed since yesterday, in five bullet points.',
    workspace: 'home',
    hosts: [],
    budgetCents: 25,
    source: 'schedule',
    }),
    }),
    ],
    } satisfies Track;
    • dailyAt: 'HH:MM' fires once a day at that time, on a 24-hour clock. Use everyMs instead for a fixed interval, for example everyMs: 15 * 60_000 for every 15 minutes. everyMs runs from 1 ms to about 24 days. A bad value stops the app from loading.
    • workspace: 'home' is the workspace every box has. Put the files the routine reads there, or name another workspace.
    • task() returns a task spec. Return nothing to skip a run, for example when there is no new work.
    • hosts: [] means the task reaches no website. Model calls go through the box. Add only the hosts the routine needs. See Network policy.
    • budgetCents: a paid tool call that would take the task past this amount asks for your OK first. Model use counts toward the total but never pauses the task by itself.
  3. Install the checkout on the box. The install script copies it to /opt/box and restarts the service, which loads every app at boot.

    sudo core/install.sh
  1. Look for the app in the service's startup line:

    journalctl -u box.service -n 20 | grep 'box up'
    box up: 12 tasks in the log, tracks: anthropic, nightly

    The line lists only apps that are on. If nightly is missing, turn it on in App Center. If the app failed to load, the journal has track nightly not loaded: and the reason. A broken app never stops the box.

  2. To see a task without waiting until 02:00, set everyMs: 60_000 for a test run, install again, and wait a minute. Then list tasks:

    curl -s http://127.0.0.1:7700/api/tasks | grep -o '"title":"Nightly notes report"[^}]*'

    Change the schedule back when you are done.

  • A run that falls while the box is off is skipped, not caught up. dailyAt arms the next occurrence after each start.
  • everyMs counts from when the box started, not from a fixed clock time.
  • dailyAt uses the box's local time zone.
  • Schedules live in app code. A screen for creating routines in the portal is not built.