> ## 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 credential 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(...)`. See [Route to the next node yourself](/operate/scripts#route-to-the-next-node-yourself).
* **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.' });
};
```

***

## Credential tokens

A script fills a secret with a `##NAME##` token, replaced from the profile just before the script runs, so the value never reaches the model. The token name is the credential key in upper case. The usage is in [Credentials in a script](/operate/scripts#credentials-in-a-script), and the full model is in [Credentials](/concepts/credentials).

***

## 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="Credentials" icon="key" href="/concepts/credentials" horizontal>How a `##TOKEN##` gets its value</Card>
  <Card title="Nodes" icon="box" href="/concepts/nodes" horizontal>Where a script file lives on a node</Card>
</CardGroup>
