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

# Beyond the catalog

> An automation is a normal script on a full Linux machine. Reach your own APIs, databases, clusters and cloud accounts with any library you like.

The [connected integrations](/guides/integrations/connections) are the convenient part: typed, pre-authenticated, and visible in the console. Everything else a machine can reach, a script can reach too. A `*.run.ts` file is ordinary TypeScript running under Bun on a machine with root, a persistent disk, Docker, Python, Go and outbound internet, so anything you can reach from that machine you can reach from an automation, with whatever client library you'd normally use.

## Install what you need

The script's directory is a normal Bun project. Add dependencies next to it and import them:

```bash theme={"theme":"github-dark"}
cd ~/automations/inventory-sync
bun add pg @aws-sdk/client-s3 @kubernetes/client-node
```

```ts theme={"theme":"github-dark"}
import { Client } from "pg";
import { every, slack } from "@boxd/run";
```

`@boxd/run` keeps resolving from anywhere, and your own dependencies resolve from the script's `node_modules`. Anything installed on the machine, CLIs included, is available too. `Bun.$` runs shell commands, so `kubectl`, `psql`, `aws`, `gcloud` or your own binaries are one line away.

```ts theme={"theme":"github-dark"}
import { $ } from "bun";

const pods = await $`kubectl get pods -n prod -o json`.json();
```

## Keep credentials out of the file

Put connection strings, tokens and passwords in [env vars and secrets](/guides/env-secrets) and read them from `process.env`. They are set once for your account or organization, injected into every machine you own, and never appear in the script. That matters because the script's source is what the console's generated write-up is built from (credential-shaped strings are redacted before that happens, but a secret that was never in the file has nothing to redact).

```bash theme={"theme":"github-dark"}
boxd env set DATABASE_URL postgres://app:…@db.internal:5432/app --secret
boxd env set KUBECONFIG_B64 "$(base64 -w0 ~/.kube/config)" --secret
```

```ts theme={"theme":"github-dark"}
const db = new Client({ connectionString: process.env.DATABASE_URL });
```

Every job on the machine sees them, and a re-run picks up new values. On a shared machine, only values with the `shared` or `all` scope are present. See [Env vars & secrets](/guides/env-secrets) for scopes.

## Reaching things

| Target                         | How                                                                                                                                                                                                           |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Your own app's API             | Plain `fetch` with a token from `process.env`. If the app runs on another boxd machine, use its private `.boxd` name over the [machine network](/guides/vm-to-vm), or its public `https://<name>.boxd.sh` URL |
| Internal services behind a VPN | Join the machine to your [Tailscale](/guides/tailscale) network once. The automation then reaches internal hostnames like any other node                                                                      |
| Databases                      | The usual driver (`pg`, `mysql2`, `mongodb`, `ioredis`, …) with a connection string from `process.env`                                                                                                        |
| Kubernetes                     | `@kubernetes/client-node`, or `kubectl` through `Bun.$`, with a kubeconfig you place on the machine or decode from a secret                                                                                   |
| Cloud accounts                 | The provider's SDK (`@aws-sdk/*`, `@google-cloud/*`, `@azure/*`) with credentials from `process.env`. The machine's outbound IP is fixed per cluster if you need to allow-list it                             |
| Anything with a CLI            | `Bun.$`. Docker is there too, so a tool you'd rather not install can run as a container                                                                                                                       |

## Example: nightly report from Postgres to Slack

```ts theme={"theme":"github-dark"}
// nightly-report.run.ts
import { Client } from "pg";
import { every, slack } from "@boxd/run";

every("0 7 * * *", async () => {                      // 07:00 UTC
  const db = new Client({ connectionString: process.env.DATABASE_URL });
  await db.connect();
  const { rows } = await db.query(
    "select count(*)::int as signups from users where created_at > now() - interval '1 day'",
  );
  await db.end();
  await slack.sendMessage({ channel: "#growth", markdown_text: `${rows[0].signups} signups yesterday.` });
});
```

## Example: react to a GitHub deploy by rolling a cluster

```ts theme={"theme":"github-dark"}
// rollout.run.ts
import { $ } from "bun";
import { githubApp, slack } from "@boxd/run";

githubApp.on("release.published", { repo: "acme/api" }, async (event) => {
  const tag = event.release.tag_name;
  await $`kubectl set image deployment/api api=ghcr.io/acme/api:${tag} -n prod`;
  await $`kubectl rollout status deployment/api -n prod --timeout=5m`;
  await slack.sendMessage({ channel: "#deploys", markdown_text: `api ${tag} rolled out to prod.` });
});
```

## Example: your own API, on a schedule, with durable state

```ts theme={"theme":"github-dark"}
// invoice-sync.run.ts
import { every, object, panic } from "@boxd/run";

const state = object<{ cursor?: string }>("invoice-sync");

every("10 minutes", async () => {
  const res = await fetch(`https://billing.internal/api/invoices?since=${state.cursor ?? ""}`, {
    headers: { authorization: `Bearer ${process.env.BILLING_TOKEN ?? panic("BILLING_TOKEN is not set")}` },
  });
  const { invoices, next } = await res.json();
  for (const inv of invoices) { /* … push to your accounting system … */ }
  state.cursor = next;                                // persisted for the next run
});
```

Everything else about [jobs](/guides/automations/jobs) applies unchanged. These scripts show up in the console with a generated write-up and diagram, retry on crashes, and wake the machine for their schedules and events. What a script does between the trigger and the result is up to you.
