> For the complete documentation index, see [llms.txt](https://docs.zerowork.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.zerowork.io/using-zerowork/using-building-blocks/write-javascript/imports-and-package-management.md).

# Imports and Package Management

***Agents with versions older than 1.1.76 have limitations (pure ESM packages and native packages are generally not supported). Install version 1.1.76 for broader package support.***

Use standard JavaScript imports like `import dayjs from "dayjs@1.11.11"` or use the `zw` API `zw.import()` for custom options to install (if needed) and import packages during a TaskBot run. ZeroWork installs missing packages and makes them available to your code.

Manage packages with `zw.packages.list()`, `zw.packages.uninstall()`, and `zw.packages.uninstallAll()`.

**API** `zw.import()`, `zw.packages.*`

```javascript
// @zw-run-locally

// Use desktop automation in ZeroWork

import { execSync } from "child_process";
import { keyboard } from "@computer-use/nut-js";

// Open an editor depending on OS
if (process.platform === "win32") {
  execSync("start notepad");
} else if (process.platform === "darwin") {
  execSync(`osascript -e 'tell application "TextEdit"' -e 'launch' -e 'activate' -e 'repeat until it is running' -e 'delay 0.2' -e 'end repeat' -e 'make new document' -e 'end tell'`);
} else {
  execSync("gedit || xed || kate || nano", { shell: "/bin/bash" });
}

// Wait briefly for the editor window to appear
await zw.delay({ min: 2_000 });

// Type into the editor
await keyboard.type("Hello from ZeroWork desktop automation!");
```

***

## 1. Imports

### Standard Imports

You can use standard ESM or CommonJS syntax. Any such import installs the package if needed, then loads it.

```javascript
// @zw-run-locally

// ESM default import
import dayjs from "dayjs@1.11.11";
await zw.log("now", dayjs().toISOString());

// ESM subpath import
import chunk from "lodash/chunk";
await zw.log("chunked result", chunk([1, 2, 3, 4], 2));

// CommonJS require
const _ = require("lodash@4.17.21");
await zw.log("uniq result", _.uniq([1, 1, 2, 3]));

// Import from Git (HTTPS only)
import Chance from "git+https://github.com/chancejs/chancejs.git";
```

**Notes**

* Installed packages are available to any TaskBot by default. To scope per TaskBot, use `zw.import()` with `{ isolate: true }` (see details further below).
* If a package isn’t used for one week, it's removed automatically. To change this time window or prevent automatic uninstall, use `zw.import()` with `uninstallIfUnusedFor` (see details further below).
* Subpaths like `"lodash/chunk"` and Git repositories are supported.
* You can disable auto-imports by adding a comment `// @zw-disable-auto-import`

  ```javascript
  // @zw-disable-auto-import

  import dayjs from "dayjs@1.11.11"; // throws error
  ```

***

### Imports with `zw.import()`&#x20;

Use `zw.import()` when you need options or want to import several packages at once.

#### **At a Glance**

* <mark style="color:$info;">async</mark> \
  `await zw.import(pkg: string | string[] | { [key: string]: string }, options?: ImportOptions)` \
  → installs if needed and returns the loaded module(s)

```typescript
// Reference only — not runnable in Write JS
type ImportOptions = {
  uninstallIfUnusedFor?: number | null; // hours; default 168 (1 week); null = never uninstall
  isolate?: boolean;                    // scope to this TaskBot only; default false
  preferDefault?: boolean;              // advanced; default true
};
```

#### **Examples**

**Single package with options**

```javascript
// @zw-run-locally
const dayjs = await zw.import(
  "dayjs@1.11.11",
  { isolate: true, uninstallIfUnusedFor: 300 }
);
```

**Multiple packages as an array**

```javascript
// @zw-run-locally
const [dayjs, lodash] = await zw.import(
  ["dayjs@1.11.11", "lodash@4.17.21"]
);
```

**Named mapping as an object**

```javascript
// @zw-run-locally
const { time, dash } = await zw.import(
  { time: "dayjs@1.11.11", dash: "lodash@4.17.21" }
);
```

**Installing from Git (HTTPS only)**

```javascript
// @zw-run-locally
const gitChance = await zw.import(
  "git+https://github.com/chancejs/chancejs.git"
);
```

**Never uninstall**

```javascript
// @zw-run-locally
const lodash = await zw.import(
   "lodash@4.17.21",
   { uninstallIfUnusedFor: null }
);
```

#### **Import Options**

* **`uninstallIfUnusedFor` (default: `168`)**\
  Defaults to 168 hours (**one week**). Set to `null` to keep the package on the device indefinitely.
* **`isolate` (default: `false`)**\
  When `true`, the package is scoped to this TaskBot only and isn’t visible in other TaskBots. Useful when different TaskBots need different versions of the same package. Note: If the TaskBot is deleted, the package isn’t removed automatically; you can manage it later with `zw.packages.*` (see details further below).
* **`preferDefault` (default: `true`)** — *advanced*\
  Returns `module.default` when present (otherwise the module object). This only changes the return shape; it doesn’t affect how the module is resolved.

  ```javascript
  // @zw-run-locally

  // In most cases, leave preferDefault as is (true).
  const mysql = await zw.import("mysql2@latest/promise"); // works
  const { chalk } = await zw.import({ chalk: "chalk@4" }); // works
  ```

#### **Supported Package Inputs**

You can pass any of the following to `zw.import()` as the first argument (package input).

* **String**\
  `"lodash"`, `"lodash@4.17.21"`, `"lodash/chunk"`,\
  `"https://github.com/user/repo.git#main"` or `"git+https://github.com/user/repo.git#main"`
* **Array of strings**\
  `["dayjs@1.11.11", "lodash@4.17.21"]`
* **Object mapping**\
  `{ util: "lodash@4.17.21", time: "dayjs@1.11.11" }`

**Invalid inputs that are rejected**

* Tarball URLs, local file paths, and directories
* Non-HTTPS Git URLs

***

### Built-in Packages

The following packages and Node core modules are pre-bundled and resolve without installation.

* Node core modules such as `fs`, `os`, etc.
* `axios` pinned to `^1.6.6`
* `playwright` pinned to `^1.45.0`

They don’t appear in `zw.packages.list()`, and you can’t change their versions or uninstall them.

```javascript
// @zw-run-locally

import * as fs from "fs";  // pre-bundled; no installation occurs
import axios from "axios"; // resolves to axios@^1.6.6

// Version spec is ignored; resolves to pinned playwright@^1.45.0
import playwright from "playwright@1.46.0";
```

#### **Note on Playwright**

The pre-bundled `playwright` import gives you the Playwright API, but the Agent does not bundle the browser binaries Playwright normally downloads. TaskBots run on the Chrome (or the executablePath of another Chromium-based browser) installed on your device. So `playwright.chromium.launch()` with no options fails; launch with the Chrome channel instead: `await playwright.chromium.launch({ channel: "chrome" })`. That said, in most cases you don't need this because you can use [`zw.browserContext.launch()`](/using-zerowork/using-building-blocks/write-javascript/browser-context.md).

***

### Reusing References

Import once and reuse the reference, as is generally best practice. The package won’t reinstall if it’s already installed, but package parsing and resolution still add overhead.

```javascript
// @zw-run-locally

// 1. Anti-pattern: import inside the loop (re-parses/resolves each time)
for (let i = 0; i < 1_000_000; i++) {
  await zw.log((await zw.import("dayjs@1.11.11"))().toISOString());
}

// 2. Recommended: reuse a local reference
const dayjs = await zw.import("dayjs@1.11.11");
for (let i = 0; i < 1_000_000; i++) {
  await zw.log(dayjs().toISOString());
}

// 3. Optional (micro-optimization): reuse across Write JS blocks via run state
const cachedDayjs = await zw.import("dayjs@1.11.11");
const state = zw.state.access();
if (!state.cachedResolvedImports) {
  state.cachedResolvedImports = {};
}
state.cachedResolvedImports.dayjs = cachedDayjs;

// In a later building block:
await zw.log(zw.state.access().cachedResolvedImports.dayjs().toISOString());
```

***

## 2. Package Management

#### At a Glance

* <mark style="color:$info;">async</mark> \
  `await zw.packages.list()` \
  → returns `string[]` of **package IDs**.
* <mark style="color:$info;">async</mark> \
  `await zw.packages.uninstall(id: string)` \
  → removes one.
* <mark style="color:$info;">async</mark> \
  `await zw.packages.uninstallAll()` \
  → removes all user-managed packages.

{% hint style="info" %}
Built-ins (`axios@^1.6.6`, `playwright@^1.45.0`, and Node core modules like `os`, `fs`, etc.) do not appear in `list()` and cannot be uninstalled.
{% endhint %}

#### What’s a Package ID?

A **package ID** uniquely identifies an installed package instance. It’s an opaque string made of an optional isolation prefix (`<taskbotId>_`), a lower-cased, sanitized package name, `@`, and a version tag (`<semver>`, `latest`, `git`, or a commit hash). It may differ from your original import string (subpaths aren’t included; characters are normalized).

**Examples**

```typescript
lodash@4.17.21
dayjs@1.11.11
12345_dayjs@1.11.0   // isolated to a specific TaskBot (prefix includes its ID)
repo_name@git        // Git source without a specific commit
repo_name@9f3a2c4    // Git source pinned to a commit
```

Use these IDs with `zw.packages.uninstall(id)`.

***

#### Examples

**List packages**

```javascript
// @zw-run-locally

const ids = await zw.packages.list();
await zw.log("packages", ids);
```

**Uninstall by matching name/version in the package ID**

```javascript
// @zw-run-locally

const ids = await zw.packages.list();
const target = ids.find(id => id.includes("lodash@4.17.21"));
if (target) await zw.packages.uninstall(target);
```

**Uninstall all for a specific TaskBot**

```javascript
// @zw-run-locally

const myTaskBotId = 12345;
const ids = await zw.packages.list();
const targets = ids.filter(id => id.startsWith(`${myTaskBotId.toString()}_`));
for (const target of targets) {
  await zw.packages.uninstall(target);
}
```

**Uninstall all**

```javascript
// @zw-run-locally

await zw.packages.uninstallAll();
```

***

## 3. Local and Browser Execution

Package installation and management run locally only. Standard `import`/`require`, `zw.import()`, and `zw.packages.*` are supported **only** when the Write JS block runs outside the browser context. Enable the **Run locally** checkbox in the block UI, or add the comment `// @zw-run-locally`.&#x20;

✅ You can still ***use*** imported packages in the browser by exposing functions from a local block (see below).

### How to Use Imported Packages in the Browser

Here's how:

* Import in a block that runs locally (e.g. use `@zw-run-locally`) and expose the needed function.
* Call the function from a later block that runs in the browser.

```javascript
// Write JS Block A — runs locally

// @zw-run-locally 
import lodash from "lodash";

// Illustrative for simplicity (but prefer pattern below)
const context = zw.browserContext.getContext();
await context.exposeFunction("exposedChunkFn", (arr) => lodash.chunk(arr, 2));

// Better pattern for context continuity
// Ensure function is exposed on any (re)launch
await zw.browserContext.setDefaults({
  onContextReady: async (context) => {
    await context.exposeFunction("exposedChunkFn", (arr) => lodash.chunk(arr, 2));
  }
});
```

```javascript
// Write JS Block B — runs in the browser

const result = await exposedChunkFn([1, 2, 3, 4]); // [[1,2],[3,4]]
await zw.log("exposedChunkFn result", result);
```

{% hint style="warning" %}
Functions exposed via `context.exposeFunction()` always become **asynchronous** in the browser, even if they were defined as synchronous. Always call them with `await`.
{% endhint %}

***

## 4. Troubleshooting

#### **I need a package inside an in-browser execution but I can't import it there.**

See [#id-3.-local-and-browser-execution](#id-3.-local-and-browser-execution "mention"). That section explains how you should import it in a locally running Write JS block first and then use the imported package's function(s) in a Write JS block running in the browser.

#### Some packages require extra pre-installed tools on your machine.

In such cases, the error typically mentions "node-gyp" or "gyp ERR!" like in this example:

> The package "robotjs" was not installed. Error details: command failed. (pkg: robotjs\@0.6.0, event: install, code: 1, script: node-gyp rebuild, stderr: ...**gyp ERR!** ...)

This usually means that the package tried to compile native code and the required build tools may be missing, such as Xcode Command Line Tools (Mac), Visual Studio Build Tools with C++ (Windows), or build-essential (Linux).

Note that the "node-gyp" / "gyp ERR!" error can also happen for other reasons: the package may not support the Agent's Node version (see below: **Node version mismatch...**), may not support your device's CPU architecture, may depend on a missing system library, or may contain a bug itself. The `stderr:` part of the error contains the compiler output that usually identifies the actual problem, so check it first before installing anything.

#### Node version mismatch: Package fails with a syntax error or 'X is not a function'.

The package may require a newer Node.js version than the Agent ships with (currently Node 22). Packages state their supported Node.js versions in the `engines` field of their `package.json`, usually also in their release notes. The failure can take different forms: a syntax error, a missing API at runtime ("X is not a function"), or a load error. The fix is to pin the newest version of the package whose `engines` range includes Node 22. If no such version exists, use an alternative package.

#### **I'd like to import from a local file.**

Local file paths, directories, and tarballs are not supported in `zw.import()` or standard `import`/`require` statements.

Instead:

* For your own code, push it to a Git repository and import the HTTPS Git URL. Bonus: This works on every device the TaskBot runs on.
* Load the file directly with Node:<br>

  ```javascript
  // @zw-run-locally

  const { createRequire } = await zw.import("node:module");
  const req = createRequire("/"); // don't rename to "require"; require() calls in Write JS don't accept file paths
  const myLib = req("/Users/<username>/my-scripts/my-lib.js");
  ```

#### **The import takes forever and never resolves.**

Large packages legitimately take a while. Anything that downloads a browser (`puppeteer`, `@playwright/browser-*`) moves **hundreds** of MB on first install and can take several minutes to resolve.

> 💡When installing browser automation libraries like `puppeteer`, prefer installing them **without** browser binaries. For example, you can install `puppeteer-core`, which can turn a 500 MB installation into just a few MB. Then you can launch with your already installed browser: `await puppeteer.launch({ channel: "chrome" })`.

Separately, some package versions can ship a faulty install script. For example, some older `puppeteer` releases (v21 and earlier) stall at the very end of installation even though the download itself succeeds. (Note: current `puppeteer` versions install cleanly.)

If an install shows no progress for 15+ minutes, stop the run and retry with a different package version. If that doesn't help and you can't find any publicly reported installer bugs or issues for the package, file a bug report and we'll investigate: [Report a bug](/support/getting-support.md#report-a-technical-error-broken-feature-or-unexpected-error).

#### Error: "...This git repository requires a build step, which needs the npm command..."

This typically happens with imports from Git URLs. The repository declares a build step (a `build`, `prepare`, or `install` script in its `package.json`) that the Agent cannot run.

**What to do:**

* If the package is published to the npm registry, import it by name instead of using a Git URL. Published registry packages usually include the built output and don't require the repository build step.
* If you're on Windows or Linux, install Node.js (which includes npm) and restart the Agent. Note that this won't help on macOS, where desktop apps don't see the PATH locations Node.js installs into.
* For your own private repositories, commit runnable code and remove build-related scripts from `package.json` (i.e. commit the built output if there is a build).

#### Error: "The package ... was not installed. ... command failed (pkg: ..., stderr: ...)".

The `pkg:` field names the dependency whose install script actually failed. This is often a transitive dependency, not the package you imported. The `stderr:` output contains the underlying reason. You can investigate the issue using that error with AI and/or file a bug report for us to investigate: [Report a bug](/support/getting-support.md#report-a-technical-error-broken-feature-or-unexpected-error).

#### Browser-only packages: "...likely targets browsers".

Some packages are built to run inside a web page, not in Node. Their code needs a browser environment. Importing them locally fails with an error like:

> ... This package appears to be built for browsers rather than Node.

✅ These packages can still be used.

Here's how:

* Find the package's bundled build. (Many ship one in their `dist/` folder; look for a plain script, not raw ESM source.)
* Save it locally.
* Add it under **Scripts** in Browser Launch Settings (or in [Launch Browser](/using-zerowork/using-building-blocks/launch-browser.md) block, or via `await zw.browserContext.launch({ launchConfig: {`` `**`scripts: [{ path: "/Users/me/scripts/guard.js" }]`**` ``} })` as documented in [launchConfig.scripts](https://docs.zerowork.io/using-zerowork/using-building-blocks/write-javascript/browser-context#scripts-launchconfig.scripts)).

**Example: SingleFile**

Here's an end-to-end example with SingleFile (captures a complete web page as one HTML string):

1. **Step 1:** Save the bundle to your machine. You can use the terminal, download it manually, or use a Write JS block. What matters is saving a runnable script: SingleFile publishes its bundle wrapped in a JavaScript string (made for programmatic injection), so it needs one unwrap step. The example below downloads, unwraps, and saves it for you.

> 💡 This unwrap step is specific to SingleFile. Most browser-oriented packages ship a ready-to-use bundle you can download as-is. Look for a `dist/` folder in the repository or fetch it from a CDN like jsdelivr (<https://cdn.jsdelivr.net/npm/\\><package>/dist/...).

```javascript
// @zw-run-locally
import * as fs from "fs";
import * as os from "os";
import axios from "axios";

// SingleFile specifically publishes its bundle wrapped in a JS string; this unwraps it
const res = await axios.get(
  "https://raw.githubusercontent.com/gildas-lormeau/single-file-cli/master/lib/single-file-bundle.js"
);
const bundle = new Function(
  res.data.replace(/export\s*\{[^}]*\};?\s*$/, "") + "\nreturn script;"
)();

const dir = os.homedir() + "/zerowork-scripts";
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(dir + "/single-file.js", bundle);
await zw.log("Saved bundle to", dir + "/single-file.js");
```

2. **Step 2:** In your TaskBot's Browser Launch Settings, open the Scripts section, add a script with type Path and the file path from Step 1.
3. **Step 3:** Use it from a browser-context Write JS block. The script runs on every page the TaskBot opens, so its `singlefile` global is available:

```javascript
// Write JS with in-browser execution

const data = await singlefile.getPageData({ removeHiddenElements: true });
await zw.log("captured bytes: ", data.content.length);
await zw.log("title: ", data.title);
```
