Skip to main content
run <file>.run.ts runs a script. What happens next depends on what the script did while its top level ran.

One-shot or long-runner

  • One-shot: the script never called .on(...) or every(...). It runs like a command. Output streams to your terminal, it returns with the script’s own exit code, and it leaves nothing behind whether it succeeded or failed. Nothing is registered and nothing retries.
  • Long-runner: the script registered a trigger or a schedule. Its top-level output streams to your terminal until it settles, then it is moved to the background as a job and your shell returns:
The detection is automatic. There is no flag.

Identity: one file, one job

A job is identified by its file’s absolute path, and its 5-character id is derived from it. Running the same file again replaces the running job with the new version, under the same id. That is the deploy flow: edit, run the file, done. Subscriptions and schedules don’t churn on a replace.
Jobs survive reboots. Everything in the background or retrying state is started again when the machine boots. A job’s log is one file per job, rewritten each time the job (re)starts, and run logs -f notices and re-prints from the top.

What a job runs with

A job is started by the machine, and it inherits more than a bare environment: Jobs currently run as root, so files they create are owned by root even under /home/boxd. Use sudo chown boxd:boxd if you need to edit them from a shell afterwards.

States

Waiting on a connection

Before starting a script, boxd works out which connections it needs: every integration it imports, at the scope it uses, across all local imports. Anything missing is reported up front, and the job waits instead of failing at its first call:
Connect the named integration (the console’s Integrations page, or run auth linear) and the job starts by itself. The message always names the scope. A personal Linear does not satisfy linear.scoped("ops"). The console shows the same job as Waiting to connect.

Crashes and retries

A long-running job that exits with an error is restarted with exponential backoff (2s, 4s, 8s, 16s) up to five attempts, then parked as failed with the log kept for inspection. A one-shot is never retried and just reports its exit code. If the job crashes while you’re still watching the first run, your terminal comes back at the first retry rather than waiting out the ladder:
When the ladder is exhausted:
A failed job is not restarted on boot or after a fork. That’s deliberate, so a crash loop can’t hide by resetting itself. Fix the script and re-run the file, or run restart <id>. Errors thrown inside a trigger or schedule handler are logged and don’t count as crashes. Long-runners should be restart-tolerant. boxd restarts them on fork, snapshot restore, ownership change, and whenever the team’s set of integrations changes. Nothing in flight is preserved across a restart, so put anything that must survive in object(...).

Sleep and wake

A machine with automations still auto-suspends and hibernates like any other. An idle job makes no network calls of its own, so it never keeps the machine awake.
  • Schedules: the platform holds the next time any every(...) on the machine is due and wakes a hibernated machine shortly before it. A cron slot that a late wake slipped past fires once on wake.
  • Events: a trigger firing wakes the machine and delivers the event. Events that arrive while the machine is waking are held for up to 15 minutes. Anything older than that is dropped rather than delivered arbitrarily late.

Forks and snapshots

A forked machine, or one restored from a snapshot, inherits every job of its source and starts them immediately (except failed ones). The console write-up and diagram come along, since they’re keyed by the script’s content. Forking is the way to run the same automation many times over.

The console

Automations in the sidebar lists every long-running job across your machines. The page is read-only. The files on the machine are the source of truth. Each card shows:
  • a generated name (editable, click the pencil on the detail view) and one-line summary.
  • Triggers in plain English (Every morning at 9am, Linear issue created) and Uses, the integrations it calls, with their logos.
  • a status pill: On (or Off), Waiting to connect, Retrying (2/5), Failed, Stopped.
  • the last run time. A waiting card says exactly what it’s waiting on, and a failed card shows the last error and the run logs <id> command to inspect it.
A banner at the top counts automations that need attention. Filters: All, Active, Needs attention. Search matches the script’s name and file path. Open a card for the detail view: the machine and file, How it flows (a pan-and-zoom diagram of triggers, schedules, integrations and the machine, with labelled arrows and a description on hover over each node) and a write-up in four sections, Overview, Behaviour, Integrations and Notes. The same cards appear on each machine’s Automations tab, and its Overview shows compact tiles.

Generated documentation

The name, summary, trigger wording, diagram and write-up are generated automatically a few minutes after an automation first runs, from the script and its local imports. Before a script’s source leaves the machine, anything that looks like a credential (API-key prefixes, tokens, long random strings) is redacted, and the write-up treats those as opaque secrets. When you change the script, the documentation is regenerated with the previous version as reference, so names and diagram layout stay stable rather than reshuffling on every edit. Very large scripts are summarized from a capped portion of the source and the write-up says so. Only long-running jobs are documented. A one-shot script leaves nothing behind.