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

# Script runtime

> The runtime contract for a node script: the entry signature, timeouts, the sandbox, the filesystem, how require resolves, and secret tokens.

This page is the exact contract a node script runs under. For how to attach a script and route from one, start with [Script a step](/operate/scripts).

***

## The entry function

A script exports one async function. The runtime calls it with a single object.

```javascript theme={null}
const asteroid = require('asteroid');

module.exports = async ({ page, browser, context, args }) => {
  // page     - the active Playwright Page, on the live browser session
  // browser  - the Playwright Browser
  // context  - the Playwright BrowserContext
  // args     - the node's resolved inputs (present when the node has an input schema)

  // Golden path: end with a handoff, which chooses the next node.
  return asteroid.handoff({ to: 'next_node', summary: 'Did the work.' });
};
```

| Field | What it is |
| - | - |
| `page` | The active [Playwright](https://playwright.dev) `Page`, already on the live session. |
| `browser` | The Playwright `Browser`, for a second tab or context. |
| `context` | The Playwright `BrowserContext`. |
| `args` | The node's resolved inputs. See [Pass data into a script](/operate/scripts#pass-data-into-a-script). |

Notes on the shape:

* **CommonJS is the norm:** `module.exports = async (...) => { ... }`. `export default` works too, and an ESM file with top-level `await` is loaded as a module.
* A node with an **input schema** must destructure `args`. The `async (page) => { ... }` form does not receive `args` and fails validation when the node declares inputs.
* The **return value** decides output and routing. **The golden path is to return `asteroid.handoff(...)`**, which chooses the next node and carries its data. An object instead becomes output variables, a string becomes `script_output`, and a throw is a failure. See [Route to the next node yourself](/operate/scripts#route-to-the-next-node-yourself).

***

## Timeouts

<Warning>
  The whole script has a hard limit of **5 minutes**. When it is reached the run is killed and treated as a failure, so the node's failure action decides what happens next. Design a script to finish well inside that.
</Warning>

Inside that budget, Playwright actions carry their own defaults, kept short so a wrong selector fails fast instead of stalling.

| Default | Value |
| - | - |
| Per-action timeout (click, fill, wait for selector) | 10 seconds |
| Navigation timeout | 30 seconds |

Raise a limit for a genuinely slow step:

```javascript theme={null}
const asteroid = require('asteroid');

module.exports = async ({ page }) => {
  page.setDefaultTimeout(30000);          // all actions on this page
  page.setDefaultNavigationTimeout(60000); // navigations on this page

  await page.click('#slow-report', { timeout: 45000 }); // one call only
  return asteroid.handoff({ to: 'report_ready', summary: 'Report loaded.' });
};
```

***

## The sandbox

A script runs in the execution's sandbox, next to the browser.

* **Node.js 22.** Node built-in modules (`node:fs`, `node:path`, `node:crypto`, and the rest) are available.
* **Playwright** drives a headless Chromium over the live session. You get it through `page`, `browser`, and `context`, so you do not launch a browser yourself.
* **`python3`** is present, with `python3-yaml`, reachable from the workflow's Bash tool.
* **No PDF or OCR libraries.** To read a PDF or a print-only page, parse it in the browser page with `pdf.js`, not with a Node module. See [the PDF pattern](/operate/scripts#a-document-that-arrives-as-a-pdf-or-a-print-only-page).

***

## The filesystem

The working directory is `/home/agent`. A script reads and writes files with `node:fs`, using absolute paths.

| Directory | What it holds |
| - | - |
| `/home/agent/shared/` | The workflow's own files, placed when the workflow was built: the node scripts themselves, plus any library or reference data they read, such as a bundled parser or a field map. Read these from a script. |
| `/home/agent/workspace/` | Scratch for the current execution. It survives every step and loop-back in the run, then is cleared. Use it for within-run state. |
| `/home/agent/downloads/` | Where browser downloads land, for the current execution. |
| `/home/agent/uploads/` | Files the caller staged for this execution. |

A script's own file lives under `/home/agent/shared/<node-slug>/`, where the runtime derives `<node-slug>` from the node's display name. Because it re-derives the slug, a rename never breaks how the runtime finds the entry script. It does not, however, rewrite absolute paths you wrote by hand. A hardcoded `/home/agent/shared/<node-slug>/...` path (a required helper, a reference file) breaks when you rename the node, so update those yourself.

<Warning>
  Do not use `shared/` to carry state from one run to the next. A running script's writes to `shared/` are not saved back to the workflow. Durable `shared/` content comes from how the workflow is built, not from a running script. Keep per-run state in `workspace/`, and treat `shared/` as read-only reference.
</Warning>

<Card title="Workflow filesystem" icon="folder" href="/concepts/filesystem" horizontal>The full directory layout, the quotas, and how files get in and out</Card>

***

## Requiring modules

* **`require('asteroid')`** is provided by the runtime. Its whole surface is `handoff(...)` and `generateTotp(...)`. See [Route to the next node yourself](/operate/scripts#route-to-the-next-node-yourself) and [Two-factor codes](#two-factor-codes).
* **Node built-ins** resolve normally.
* **Helper modules** must be required by their **absolute path** under `shared/`, for example `require('/home/agent/shared/file_claim/lib/dates.js')`. A relative `require('./dates.js')` does not resolve, because the runtime runs your script from a private copy that holds no sibling files.
* **Use absolute `/home/agent/...` paths.** They work exactly as written from the entry script. Pass a required helper the paths it needs as arguments, rather than letting it compute its own, so it never depends on where the runtime placed your script.

```javascript theme={null}
const asteroid = require('asteroid');
const dates = require('/home/agent/shared/file_claim/lib/dates.js');

module.exports = async ({ page, args }) => {
  const dob = dates.toEhrFormat(args.date_of_birth);
  await page.fill('#dob', dob);
  return asteroid.handoff({ to: 'saved', summary: 'Filled the date of birth.' });
};
```

***

## Secret tokens

A script fills a secret with a `##ITEM_KEY.FIELD##` token. The runtime replaces it from the
secret attached to the profile, just before the script runs, so the value never reaches the model.
The usage is in [Secrets in a script](/operate/scripts#secrets-in-a-script), and the full
model is in [Secrets & 2FA](/concepts/credentials).

### Two-factor codes

`asteroid.generateTotp(secret, options?)` returns the current authenticator code as a string. Pass
the token of a TOTP seed field. The runtime fills the token before the script runs, so the seed
stays inside the script run. Only the generated code reaches the page.

```javascript theme={null}
const asteroid = require('asteroid');

module.exports = async ({ page }) => {
  await page.fill('#otp', asteroid.generateTotp('##MY_PORTAL.TOTP_SEED##'));
  await page.click('#verify');
  return asteroid.handoff({ to: 'dashboard', summary: 'Passed 2FA.' });
};
```

| Option | Default |
| - | - |
| `digits` | `6` |
| `period` | `30` seconds |
| `algorithm` | `'sha1'`. Also `'sha256'` or `'sha512'`, in any case. |

The secret may be a bare base32 seed or an `otpauth://totp/` URI. A URI's own `digits`, `period`
and `algorithm` apply unless an option overrides them.

* A token the run does not fill throws, and the error names the token.
* An unusable seed throws a fixed message that never quotes the seed.
* Generate the code immediately before you submit it. If the site rejects it, generate a fresh code
  and retry once.

***

## Related

<CardGroup cols={2}>
  <Card title="Script a step" icon="file-code" href="/operate/scripts" horizontal>Attach a script, route from it, and pin its inputs</Card>
  <Card title="Workflow filesystem" icon="folder" href="/concepts/filesystem" horizontal>Directories, quotas, and file transfer</Card>
  <Card title="Secrets & 2FA" icon="key" href="/concepts/credentials" horizontal>How a `##ITEM_KEY.FIELD##` token gets its value</Card>
  <Card title="Nodes" icon="box" href="/concepts/nodes" horizontal>Where a script file lives on a node</Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.