> ## Documentation Index
> Fetch the complete documentation index at: https://docs.boxd.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Scripts

> Everything a *.run.ts file can import from @boxd/run: typed integrations, triggers, schedules, durable state, scopes, and the `boxd` client for managing machines.

A script is a normal TypeScript file, named `*.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](/guides/automations/jobs) for what happens next.

```ts theme={"theme":"github-dark"}
import { linear, gh, slack, githubApp, every, object, panic, boxd, browser } from "@boxd/run";
```

## 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:

```ts theme={"theme":"github-dark"}
const issue = await linear.getIssue({ issue_id: "ENG-123" });   // returns the data, or throws
await gh.createIssue({ owner: "acme", repo: "widgets", title: issue.title, labels: ["p1"] });
```

**Methods return the result directly and throw on failure.** Wrap what you can recover from in `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](/guides/automations/jobs#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:

```ts theme={"theme":"github-dark"}
import type { LinearGetIssueOutput, LinearCreateIssueParams } from "@boxd/run/linear";
```

Which integrations are exported is **per team and changes over time**. An integration becomes available to every member's scripts as soon as anyone in the org connects it, or a script declares it. Look before you call.

## Shared connections

Calls use your **personal** connection by default. To act as one of the org's [shared connections](/guides/integrations/connections#personal-and-shared-connections), bind the integration to its name with `scoped(name)`:

```ts theme={"theme":"github-dark"}
const ops = linear.scoped("ops");
const issues = await ops.listIssues({});
```

`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's `ops` connection, and a plain `linear.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 uses `scoped(...)` for an integration doesn't need a personal connection to it.

## Triggers

`.on(event, config?, handler)` subscribes to an integration's events:

```ts theme={"theme":"github-dark"}
gh.on("issue-created", { owner: "acme", repo: "widgets" }, async (event) => {
  console.log(event.data.title);    // the payload is typed per trigger
});

slack.on("channel-created", async (event) => { /* … */ });
```

Whether the config argument is required is enforced by the types. A trigger with required configuration fields makes it a required second argument, all-optional configuration makes it optional, and a trigger with no configuration takes just the handler. Autocomplete on `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:

```ts theme={"theme":"github-dark"}
every("30s", async () => { /* … */ });
every("5 minutes", async () => { /* poll */ });
every("1.5h", async () => { /* … */ });
every("0 21 * * *", async () => { /* 21:00 UTC, daily */ });
every("*/15 9-17 * * mon-fri", async () => { /* office hours */ });
every("@daily", async () => { /* … */ });
```

The interval is either a **duration** (a number and a unit: `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](/guides/automations/jobs#sleep-and-wake).

## Durable state

Module-level variables don't survive a restart. `object(name)` gives you a JSON object that does:

```ts theme={"theme":"github-dark"}
const state = object<{ cursor?: string; seen: string[] }>("github-poller");
state.seen ??= [];

state.cursor = page.next;                 // persisted immediately
state.seen = [...state.seen, issue.id];   // assign, don't push: persistence is per top-level property
```

Keyed by name, JSON-serializable values only, written on every assignment to a top-level property and restored on the next run. Ideal for cursors, dedup sets and counters. Anything heavier belongs in a real store on the machine.

## `githubApp`: GitHub without connecting anything

If your organization has installed the [boxd GitHub App](/guides/integrations/connections#the-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](/guides/automations/jobs#crashes-and-retries).

## `boxd`: managing machines

The same import carries the boxd [TypeScript SDK](/reference/typescript-sdk), already authenticated as this machine's account. Create, fork, snapshot and manage machines from a script with zero configuration. See [Managing machines](/guides/automations/managing-machines) for what it can reach and the fan-out patterns.

```ts theme={"theme":"github-dark"}
const box = await boxd.machines.create({ name: "worker" });
const out = await boxd.machines.exec(box.id, { command: ["claude", "-p", "summarize ./report.md"] });
await boxd.machines.delete(box.id);
```

## `boxd.local`: your laptop

`boxd.local` (also exported as `local`) reaches the [client utilities](/guides/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:

```ts theme={"theme":"github-dark"}
const laptop = boxd.local.device();               // or .device("desktop") by label
const files  = await laptop.list("Downloads");
const notes  = await laptop.read("notes.md");
const chrome = await laptop.browser.connect();    // a Playwright Browser, see Browsers
```

`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](/guides/automations/browser) for the browser side.

## `browser`: this machine's browser

`browser.launch()` opens this machine's own browser, visible on its [Desktop](/guides/desktop), and returns a standard Playwright `Browser`. See [Browsers](/guides/automations/browser).

## 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](/guides/automations/beyond-the-catalog).

## Structure larger automations

Put the entry file, the one with `every(...)` / `.on(...)`, at the top and the fiddly parts in helper modules:

```
my-automation/
  daily-digest.run.ts   # entry: schedules and triggers
  linear.ts             # helpers that call Linear
  format.ts             # pure formatting
```

Local imports (`./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.
