Search⌘KHide sidebarOpen menu
Switch to dark mode
Copy page content as Markdown⌘⌥C
Login
Build integrations

The sandbox

Where package code runs, what it can reach, the limits it has to fit in, and how failures look.

Every time Beetl runs code from a Connect package, it starts a fresh, isolated sandbox for that one job: a build, a connection test, a step of an OAuth sign-in, or a sync run. The sandbox is discarded when the job ends, along with anything your code held in memory or wrote to disk. Only what Beetl stores for you carries over: committed records, the sync's checkpoint, and the connection's credentials and authorization.

The sandbox protects Beetl, other tenants and other connections from package code. Within its own connection, a package can call any endpoint its credentials allow, so install only packages you trust. See The agent trust boundary.

Network access

Package code can reach hosts on the public internet over HTTPS on port 443. Everything else is blocked, including:

  • private network addresses and localhost
  • other ports, and plain HTTP
  • Beetl's internal services
  • inbound connections, so a package can't receive callbacks or webhooks

A package can't read a system that is only reachable on a private network or over a VPN. If that system can send HTTP requests, have it push data to a webhook connection.

Make every HTTP request through ctx.fetch, ctx.json or ctx.paginate. Other HTTP clients, including the global fetch, can't open connections from the sandbox. The helpers reach only the connection's origin and its OAuth endpoints. See Authentication.

A build can reach only the public npm registry. Dependencies are installed from your lockfile with install scripts turned off, so a dependency that needs a postinstall step or downloads a binary while installing won't work. Sync runs can't install anything.

Limits

Each job has its own time and memory budget:

JobTime limit
Sync run15 minutes
Build15 minutes
Connection test2 minutes
Each OAuth step2 minutes
ResourceLimit per job
Memory1 GiB
Temporary files in /tmp512 MiB, shared with your unpacked package and counted against memory

A sync run also has to fit these limits:

Per sync runLimit
Staged output1 GiB
Batches10,000
Records plus deletions in one batch10,000
Size of one batch8 MiB
Distinct primary keys (Merge)5,000,000
One provider response16 MiB
One log message4 KiB
Log text1 MiB

A run that goes over any of these fails and writes nothing. Yield whole pages as batches, not single records, or you run out of batches long before output.

If a full read might not fit in one run, bound the work. In an Append or Merge sync, stop after a set number of pages, yield a checkpoint that marks the spot, and return; the next run continues from there. A Replace run has to read everything, because it replaces the whole dataset.

The runtime

Package code runs on Node.js 24 as ES modules, with the dependencies installed at build time. Node's built-in modules, such as node:crypto, node:zlib and node:timers/promises, are available. Deno APIs are not.

The filesystem is read-only apart from /tmp, which is new for every job. There are no environment variables for package configuration. Settings reach your code through ctx.config, and the SDK adds credentials to requests as your auth definition describes. Your code has no API for reading credentials or tokens.

Logging

Write logs with ctx.log.debug, ctx.log.info, ctx.log.warn and ctx.log.error. Messages from a sync run appear on the connection's Logs tab, where they are kept for 30 days.

  • Only the message text is kept. The optional fields argument isn't shown, so put the values you need into the message.
  • After a message larger than 4 KiB, or once a run has logged 1 MiB, Beetl stops recording messages for the rest of that run.
  • Output from console.log, console.error or anything else written to stdout or stderr is not captured.
  • Don't log credentials, tokens or whole records.

Failures and retries

Inside a run, the SDK retries individual requests as described under Retries and rate limits.

If your code throws or the run hits a limit, including the time limit, the run fails and writes nothing. Checkpoints yielded during it are dropped, and the next run starts from the last committed checkpoint. Beetl doesn't rerun a run that failed in package code; the next scheduled run or Run now tries again. Failures on Beetl's side are retried automatically, each in a fresh sandbox.

The failure reason on the Runs tab doesn't include your error's message. Log the detail before you throw:

try {
  yield { records: await ctx.json("/v1/items", Items) };
} catch (error) {
  await ctx.log.error(`Listing items failed: ${error instanceof Error ? error.message : "unknown error"}`);
  throw error;
}

A job is stopped at its time limit, and cleanup code may not run. Pass ctx.signal to any waiting you do yourself, as the example does.

A connection test isn't retried, so keep verify to one small authenticated request that throws when access is wrong. OAuth steps aren't retried either; the user chooses Connect with OAuth again. A rejected refresh token puts the connection into Reauthorization required and stops its syncs until someone reconnects.

Last updated on

On this page