*.run.ts, that imports from @boxd/run. @boxd/run resolves from any directory on the machine with nothing to install, and run init drops a tsconfig.json so your editor sees the full types. Run it with run <file>.run.ts. See Jobs for what happens next.
Integrations
Every integration enabled for your team is an export named after it in camelCase:linear, slack, notion, googleMaps (for run google-maps), googlesheets. GitHub is exported as both github and gh. Each integration has one method per tool, and every parameter and result is fully typed:
try/catch. A call on an integration that isn’t connected throws with the connect link in the message. The script doesn’t need to handle that case itself (see waiting on a connection).
Discover names the same way you do on the CLI (run search "…", run <toolkit>, run <toolkit> <tool> --help) or lean on autocomplete. The full type surface of an integration lives at its own subpath:
Shared connections
Calls use your personal connection by default. To act as one of the org’s shared connections, bind the integration to its name withscoped(name):
scoped() returns the same fully-typed integration. The names you can pass are typed from the connections your org actually has, so a typo fails in the editor. "personal" is the explicit spelling of the default.
- The argument must be a string literal. A computed scope is refused when the script registers (scoped(…) needs a literal tag - a computed scope can’t be resolved before the script runs, prefixed with the file it was found in), because boxd checks up front which connections a script needs.
- Scoping applies to triggers too.
linear.scoped("ops").on("issue-created", …)listens on the org’sopsconnection, and a plainlinear.on(...)on your own (or the team’s single shared one), by the same resolution rule as calls. Each handler receives only the events of the connection it subscribed on. - An unscoped call means “whichever is mine”: your personal connection, or the org’s single shared one if that is the only option. On a shared machine only the org’s shared connections exist, so a script there either uses
scoped("<name>")or relies on there being exactly one. A script that only ever usesscoped(...)for an integration doesn’t need a personal connection to it.
Triggers
.on(event, config?, handler) subscribes to an integration’s events:
gh.on( lists the events, and hovering the handler shows the payload shape. It differs per trigger, so check rather than assume.
Handlers run once per event, asynchronously, so don’t rely on ordering. An error thrown in a handler is logged and does not crash the job. Registering a trigger is what makes the script a long-running job.
Schedules
every(interval, handler) registers a schedule:
ms, s, m, h, d, w, or their long forms like minutes, 2 hours, 1 day) or a cron expression: five fields (minute hour day-of-month month day-of-week) or six with leading seconds, with lists, ranges, steps and month/day names, plus @hourly, @daily, @weekly, @monthly, @yearly. Cron is evaluated in UTC. Use a duration for “every so often” and cron for clock times. A spec that is neither throws when the script registers it, so a typo can’t turn into a schedule that silently never fires.
Handler errors are logged, never fatal. There is no minimum interval. Schedules are what let the platform wake a sleeping machine, and a cron slot that a late wake slipped past fires once on wake rather than being lost. See Sleep and wake.
Durable state
Module-level variables don’t survive a restart.object(name) gives you a JSON object that does:
githubApp: GitHub without connecting anything
If your organization has installed the boxd GitHub App, scripts get it directly. githubApp.client() returns an authenticated Octokit, githubApp.on("pull_request.opened", { repo: "acme/widgets" }, handler) subscribes to typed webhook events, and githubApp.getToken() mints a short-lived installation token. It is organization-wide and works on shared machines.
panic(message)
Prints the message and exits the script with an error. Handy as a guard: const first = items[0] ?? panic("no items"). For a long-running job, this counts as a crash and enters the retry ladder.
boxd: managing machines
The same import carries the boxd TypeScript SDK, already authenticated as this machine’s account. Create, fork, snapshot and manage machines from a script with zero configuration. See Managing machines for what it can reach and the fan-out patterns.
boxd.local: your laptop
boxd.local (also exported as local) reaches the client utilities on your own computer: read files under your home directory, and drive a Chrome on your laptop (in a dedicated boxd profile, on your screen). Everything goes through a device, the laptop that started the script or the account’s most recently connected one:
await boxd.local.devices() lists connected laptops. An explicit selector (label, id, or unique prefix) is strict and never falls back to a different computer. See Browsers for the browser side.
browser: this machine’s browser
browser.launch() opens this machine’s own browser, visible on its Desktop, and returns a standard Playwright Browser. See Browsers.
Anything else
A script is plain TypeScript on a full Linux machine.bun add what you need and reach your own APIs, databases, clusters and cloud accounts with the usual libraries. See Beyond the catalog.
Structure larger automations
Put the entry file, the one withevery(...) / .on(...), at the top and the fiddly parts in helper modules:
./linear, ./format) are followed automatically. The connections a script needs, and the source shown in the console’s write-up, come from the whole import graph. Commented-out code doesn’t count, and aliased imports (import { slack as slk }) are understood. Test the pieces as you go. A throwaway one-shot probe.run.ts that prints a couple of calls is a fast way to confirm auth and see real data shapes before wiring the logic.