# Browser Context

Launch, quit, manage, and inspect the browser context and pages your TaskBot uses.

```js
// @zw-run-locally

// Quick start (abridged)
await zw.browserContext.launch({
  launchConfig: {
    launchOptions: { headless: true },
  },
  /* More options available */
});
await zw.log("Context info", await zw.browserContext.getContextInfo());
await zw.browserContext.quit();
```

***

### 1. Core Concepts

**Launching the browser context**

Launch with [`zw.browserContext.launch()`](#id-2.-launch-the-browser-launch) (ZeroWork-managed). This ensures:

* No-code blocks use the same context.
* Lifecycle is managed for you. To opt out of auto-quit, pass `runConfig.keepAlive: true`.
* (Re)launches reapply your launch arguments.

*Note: If you need a custom launch flow or a self-managed context, see **Advanced: Custom and self-managed contexts** below.*

***

**Active page**

A TaskBot has one active page at a time. No-code web-interaction blocks act on the active page. If you create a page in code and want no-code blocks to use it, set it with [`zw.browserContext.setActivePage(page)`](#id-7.-active-page-and-pages-setactivepage-getactivepage-isactivepage-listpages).

***

**Context (re)launch**

A TaskBot automatically (re)launches a context in these cases:

* **Open Link** building block — launches one if none exists, otherwise reuses.
* **Write JS** set to run **in the browser** — launches one if none exists, otherwise reuses.
* A call to **`zw.browserContext.launch()`** — launches one and, depending on `policy.makeMain`, either replaces the existing context or creates a parallel one.
* **Launch Browser** building block — launches one and replaces the existing context.
* Recovery after staleness or a crash.
* Performance optimizations in long-running TaskBots.

> 💡 **Common gotcha:** Closing the last tab (e.g., with **Switch** **or** **Close Tab**) ends the context. The next **Open Link** block creates a **fresh** **context** from [**defaults**](#id-3.-defaults-setdefaults-getdefaults).

Avoid surprises (e.g., “I launched a headless browser and it suddenly became headful mid-run”) with these **best practices**:

* Use `zw.browserContext.launch()` to launch. Don't use custom context launch flows like `playwright.chromium.launch()`. (If you need a custom launch flow, you can pass `contextProvider`.)
* Add cookies, scripts, and permissions via `zw.browserContext.launch()` or `zw.browserContext.setDefaults()` so that they **reapply on (re)launches**. Don't use ad hoc calls like `context.addCookies()`.

***

**Sticky mode (sticky profiles)**

[**Sticky profiles**](#id-5.-persistent-sticky-browser-profiles-clearprofile-cloneprofile) follow special rules.

* One browser instance per profile. Same `stickyProfileId` ⇒ the same live browser instance across TaskBots (multiple isolated tabs).
* Later launches attach to the live browser instance and ignore conflicting browser-level args (e.g., `headless`). Other arguments like `cookies`, `scripts` and `onContextReady` still apply on attach (deduplicated).
* Quitting from one run closes that run’s tabs, but if other TaskBots are still using the browser instance it remains running and only quits when the last user leaves.

***

**Lifecycle**

Contexts are closed and cleared automatically.

* When a run ends, the context and browser quit unless `runConfig.keepAlive` is `true` or a shared sticky profile is still in use by other TaskBots.
* If `runConfig.keepAlive` is `true` but there are no open or eligible tabs, the context and browser still quit.
* If the browser stays open because of `runConfig.keepAlive`, the Desktop Agent cleans up when it detects manual closure (for example, when you close the window by clicking **X**). The listener also stays active after a TaskBot run, so cleanup is guaranteed even when no TaskBot is running.

> ⚠️ **Caution**: `headless: true` + `keepAlive: true` can leave an invisible instance consuming resources or, if `mode: "sticky"`, blocking a sticky profile. The UI blocks this combination, but the API allows it—use with care.

***

**Advanced: Custom or self-managed contexts**

* **Custom launch flow**\
  Provide a callback via [`contextProvider`](#context-provider-contextprovider-advanced) in `zw.browserContext.launch()` when you need options the standard `zw` API doesn’t cover (e.g., launching with Firefox). See examples [here](#context-provider-contextprovider-advanced).
* **Self-managed context**\
  Pass `policy.makeMain: false` in `zw.browserContext.launch()` to opt out of lifecycle and relaunch policy. No-code blocks keep using the main context. To rejoin the managed flow later, use [`zw.browserContext.adoptContext()`](#id-8.-adopt-self-managed-contexts-advanced). See examples [here](#id-8.-adopt-self-managed-contexts-advanced).

***

### 2. Launch the Browser — `launch()`

#### At a Glance

* <mark style="color:$info;">async</mark>\
  `await zw.browserContext.launch(args: CustomLaunchArgs)` \
  → launches a new browser context and returns it.

#### **Launch Arguments** — **`CustomLaunchArgs`**&#x20;

```ts
// Type reference — not runnable in Write JS
type CustomLaunchArgs = {
  launchConfig?: {
    mode?: "incognito" | "sticky"; // default: "incognito"
    stickyProfileId?: number;      // used only when mode="sticky" (ignored when mode="incognito")
    maximize?: boolean;            // default: true
    bypassDetection?: boolean;     // default: false
    cookies?: Array<CookieObject> | Array<Array<CookieObject>>;
    scripts?: Array<{ path: string } | { content: string }>;
    launchOptions?: LaunchOptions;  // headless, proxy, args, executablePath, etc.
    contextOptions?: ContextOptions;// viewport, permissions, userAgent, extraHTTPHeaders, etc.
    onContextReady?: (ctx: BrowserContext) => void | Promise<void>;
    contextProvider?: () => Promise<BrowserContext> | BrowserContext; // advanced: callback that returns a custom-launched context
  },
  runConfig?: {
    keepAlive?: boolean;            // default: false
    bringToFront?: boolean;         // default: true
  },
  policy?: {
    makeMain?: boolean;             // default: true
    inheritDefaults?: boolean;      // default: true
    setAsDefaults?: boolean;        // default: true if makeMain=true, else false
  },
};
```

**Defaults used when you pass no arguments**

All options are optional. If you call `zw.browserContext.launch()` with no arguments, it inherits the Browser Launch Settings configured for this TaskBot or any values previously set via `zw.browserContext.setDefaults()`. If you set `policy.inheritDefaults: false`, any option you don’t specify falls back to the built-in baseline defaults (the `default:` comments in the type definitions).

Note how `mode` affects the default baseline when `policy.inheritDefaults: true`:

* If you don’t pass `mode`, `mode` and its corresponding settings are inherited from the current defaults.
* If you pass `mode: "sticky"`, defaults are taken from the sticky profile settings for the `stickyProfileId` you provide.
* If the current defaults use `mode: "sticky"` but you pass `mode: "incognito"`, ZeroWork switches to the built-in baseline defaults and ignores the current defaults.

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

// ✅ Works: launch without passing any arguments
await zw.browserContext.launch();
```

***

#### Mode — `launchConfig.mode` and `launchConfig.stickyProfileId`

* **`mode` (default: `"incognito"`)**

  Selects how ZeroWork launches and manages the browser context.

  * `"incognito"`: Launches an isolated browser session.
  * `"sticky"`: Launches in a **sticky browser profile**. Multiple TaskBots with the same `stickyProfileId` share the same browser instance in parallel tabs (see [**Sticky Profiles**](#id-5.-sticky-browser-profiles-clearprofile-cloneprofile-listprofiles) further below).
* **`stickyProfileId`** \
  Selects which sticky profile to use (only relevant in sticky mode). Ignored when `mode: "incognito"`.

***

#### Maximize Flag — `launchConfig.maximize`

* **`maximize` (default: `true`)**\
  Maximizes the window. Ignored in `headless` mode (`headless` must use an explicit `launchConfig.contextOptions.viewport`, see [**Context Options**](#context-options-contextoptions) further below).

***

#### Bypass Detection Flag — `launchConfig.bypassDetection`

* **`bypassDetection` (default: `false`)**\
  Hardens bot detection bypass measures and keeps TaskBots indistinguishable from human users to Cloudflare and similar anti-bot systems. (Called previously `powerMode` in beta versions.)
  * **ℹ️ Trade-offs**: In `bypassDetection` mode, some launch and context options are unavailable (see [**Launch Options**](#launch-options-launchoptions) and [**Context Options**](#context-options-contextoptions) further below) and file uploads larger than \~50 MB are blocked (downloads are unaffected).

***

#### Cookies — `launchConfig.cookies`

Provide cookies **up front** so they are reapplied on every context (re)launch.\
Each cookie must include at least `name`, `value`, and `domain` (and `path`, where relevant).

You can either provide the cookie array or, if you have cookies copied from *multiple* websites, an array of cookie arrays.

> 💡 **Tip!** Avoid adding cookies in code later via `context.addCookies()` — those won’t automatically reapply on context relaunch.

**Example**

```js
// @zw-run-locally
await zw.browserContext.launch({
  launchConfig: {
    cookies: [ /** cookie array copied from a website **/ ],
  }
});

// Multiple cookies
await zw.browserContext.launch({
  launchConfig: {
    cookies: [
      [ /** cookie array for website 1 **/ ],
      [ /** cookie array for website 2 **/ ],
      [ /** cookie array for website 3 **/ ],
    ]
  }
});
```

***

#### Scripts — `launchConfig.scripts`

Scripts are injected **before** any page loads and reinjected on context (re)launch.\
Each script item is either `{ path }` or `{ content }` (when using `path`, provide an **absolute** path).

> 💡 **Tip!** Avoid adding scripts in code later via `context.addInitScript()` — those won’t automatically reapply on context relaunch.

**Example**

```js
// @zw-run-locally
await zw.browserContext.launch({
  launchConfig: {
    scripts: [
      { content: `window._usedByTaskBot = ${(await zw.getTaskbotInfo()).id};` },
      { path: "/Users/me/scripts/guard.js" },
    ]
  }
});
```

***

#### Launch Options — `launchConfig.launchOptions`

```ts
// Type reference — not runnable in Write JS
type LaunchOptions = {
  args?: string[];           // Chrome/Chromium flags; use only for advanced use cases
  executablePath?: string;   // Chromium-based browser path; defaults to Chrome
  headless?: boolean;        // default: false
  proxy?: {
    server: string;          // "host:port" or "socks5://host:port"
    bypass?: string;         // comma-separated domains
    username?: string;
    password?: string;
  };
  // When launchConfig.bypassDetection is false, more options are available (e.g., channel).
};
```

* **`args`**\
  Use with care: custom flags can break bot detection bypass measures.\
  Note: `--user-data-dir` and `--profile-directory` are reserved. When `bypassDetection` is `true`, `--remote-debugging-port` is also reserved. If you pass a reserved flag (for example `--user-data-dir`), that flag is ignored.
* **`executablePath`** \
  By default, **your Chrome path is auto-detected** and used. You can override it with any Chromium-based browser (e.g., Chrome, Brave, Vivaldi, Chromium). You can find the executable path by opening chrome://version in your browser.\
  \&#xNAN;*Note: Some Chromium forks (e.g., Opera) diverge too much and may not work.*
* **`headless` (default: `false`)**\
  Runs in the background and uses fewer resources.
* **`proxy`**\
  For SOCKS5, prefix with `socks5://`. SOCKS5 auth isn’t supported — `username` and `password` apply to HTTP proxies only.
* **More options when `bypassDetection` is disabled**\
  When `launchConfig.bypassDetection` is `false`, Playwright’s `BrowserType.launch` options are available. For the full list, see **launch → Arguments** [**here**](https://playwright.dev/docs/api/class-browsertype#browser-type-launch).

**Example**

```js
// @zw-run-locally
await zw.browserContext.launch({
  launchConfig: {
    launchOptions: {
      headless: true,
      proxy: {
        server: "123.45.67.89:3128",
        username: "username",
        password: await zw.deviceStorage.get("proxy_password"),
      },
    },
  },
});
```

***

#### Context Options — `launchConfig.contextOptions`

```ts
// Type reference — not runnable in Write JS
type ContextOptions = {
  permissions?: string[];    // will include "clipboard-read" and "clipboard-write" by default
  userAgent?: string;        // dangerous! Changing can harm anti-detection
  viewport?: { width: number; height: number } | null;
  extraHTTPHeaders?: Object<string, string>
  // When launchConfig.bypassDetection is false, more options are available (e.g., recordVideo).
};
```

* **`permissions`**\
  ZeroWork always grants `"clipboard-read"` and `"clipboard-write"` so that the Save From Clipboard building block works.
  * ⚠️ Avoid adding permissions in code later via `context.grantPermissions()` — those won’t automatically reapply on context relaunch.
* **`userAgent`**\
  Unless you know exactly what you’re doing, prefer not to change it. If you change `userAgent`, anti-detection may no longer be fully guaranteed.
* **`viewport` (default: `{ width: 1440, height: 900 }`)**\
  Takes effect when `maximize` is disabled or when `headless` is enabled.
* **`extraHTTPHeaders`**\
  Discouraged, because it can break bot detection bypass measures. Use with care.
* **More options when `bypassDetection` is disabled**\
  When `launchConfig.bypassDetection` is disabled, Playwright’s `Browser.newContext` options are available. For the full list, see **newContext → Arguments** [**here**](https://playwright.dev/docs/api/class-browser#browser-new-context).
  * ⚠️ **Warning!** Using Playwright API to set options like `geolocation`, `extraHTTPHeaders`, etc. can harm the built-in bot detection bypass measures.

**Examples**

**Headless with explicit viewport**

```js
// @zw-run-locally
await zw.browserContext.launch({
  launchConfig: {
    launchOptions: { headless: true },
    contextOptions: {
      viewport: { width: 1000, height: 600 }
    },
  }
});
```

**Record a video**

```js
// @zw-run-locally
await zw.browserContext.launch({
  launchConfig: {
    bypassDetection: false,  // disable bypassDetection to unlock recordVideo
    contextOptions: {
      recordVideo: {
        dir: "/Users/me/zw-runs/videos",
        size: { width: 800, height: 450 },
      },
    },
  }
});
```

***

#### Callback when Ready — `launchConfig.onContextReady`

Runs whenever the context is (re)launched.

**Example**

```js
// @zw-run-locally
import * as crypto from "crypto";

await zw.browserContext.launch({
  launchConfig: {
    onContextReady: async (context) => {
      // Example 1: Expose a function from an imported package
      await context.exposeFunction(
        "sha256",
        (text) => crypto.createHash("sha256").update(text).digest("hex")
      );
  
      // Example 2: Save bandwidth by blocking images
      await context.route('**/*.{png,jpg,jpeg}', r => r.abort());
    },
  }
});
```

***

#### Context Provider — `launchConfig.contextProvider` — *Advanced*

Provide a custom launch callback. Use it when you need a custom-launched context but still want no-code blocks to use it and want to benefit from lifecycle management and relaunch policy.

**Use cases**

* You must use a browser the ZeroWork launch API doesn’t support out of the box (for example, Firefox instead of Chromium).
* You need a 100% pristine context without ZeroWork’s built-in anti-detection measures or defaults.

**Example**

**Launching Firefox**

```javascript
// @zw-run-locally
import "@playwright/browser-firefox@1.45.0"; // ensures Playwright Firefox browser gets installed
import { firefox } from "playwright";

const launchFirefoxContext = async () => {
  const firefoxBrowser = await firefox.launch();
  const context = await firefoxBrowser.newContext();

  return context;
};

await zw.browserContext.launch({
  launchConfig: {
    contextProvider: launchFirefoxContext,
    cookies: [ /** cookies **/ ],
    scripts: [ /** scripts **/ ],
    onContextReady: () => zw.log("My Firefox context is ready."),
  }
});

await zw.browserContext.createPage({ url: "https://wikipedia.org" });
await zw.delay({ min: 5_000 });
```

Because you supply the context, you control `launchOptions` (headless, proxy, etc.), `contextOptions` (viewport, permissions, etc.), window size, and any anti-detection choices. ZeroWork adopts your context, applies `cookies` and `scripts`, then runs `onContextReady`. Use this only if you need full control over browser creation and understand the trade-offs.

**These arguments are ignored when `contextProvider` is set**

* `launchConfig.launchOptions`
* `launchConfig.contextOptions`
* `launchConfig.maximize`, `launchConfig.bypassDetection`

The rest of the arguments apply — including their corresponding defaults if `policy.inheritDefaults` is true or left undefined (default is true).

> ⚠️ **Exception**: Here, `mode` is always treated as `"incognito"`. If you pass `mode: "sticky"`, ZeroWork throws an error because sticky mode isn’t supported when `contextProvider` is used. Naturally, you fully control what happens inside `contextProvider`, so you *can* launch with your own persistent browser profile there (e.g., with Playwright’s `launchPersistentContext()`). ZeroWork just won’t treat it as sticky mode, meaning there’s no built-in browser instance sharing and no profile locking.

***

#### Run Config — `runConfig`

* **`keepAlive` (default: `false`)**\
  Keeps the browser open after the run ends.
  * 💡 `keepAlive: true` is ignored if, at run end, no pages remain. The context then closes. Tabs on about:blank or ZeroWork launch pages don’t count unless they are tied to Write JS in-browser code execution.
  * ℹ️ In sticky profiles, `keepAlive` applies only to **this run’s tabs**. If `true`, your tabs remain open; if `false`, your tabs close. The browser instance itself stays running as long as another TaskBot in the same profile is using it. It quits only when the last user leaves **and** no kept-alive tabs remain.
    * ⚠️ With sticky profiles, prefer leaving `keepAlive` false. After a long device sleep, the connection can drop while the browser stays open, blocking the profile until you restart the Desktop Agent or quit that browser. Only one browser instance can use a profile at a time.
* **`bringToFront` (default: `true`)**\
  Brings newly opened tabs to the front (applies to no-code blocks that open tabs).

***

#### Policy Options — `policy`

* **`inheritDefaults` (default: `true`)**\
  Inherits values from Browser Launch Settings or any values previously saved via `zw.browserContext.setDefaults()` for options you don’t specify. Note that if you pass `launchConfig.mode: "sticky"`, the inherited defaults come from the sticky profile’s settings (based on `stickyProfileId`). Likewise, if the current defaults’ mode is set to `"sticky"` but you explicitly pass `mode: "incognito"`, ZeroWork falls back to the built-in baseline defaults and ignores the current defaults.\
  **Example**: If Browser Launch Settings have **Run in background** on and you don’t set `headless` in `launchConfig.launchOptions`, `headless` will be inherited as **`true`**.
* **`makeMain` (default: `true`)** — *advanced*\
  If `true`, the launched context becomes the main context. The previous main context is closed (unless its `runConfig.keepAlive` setting is `true`). If you set it to `false`, you partially lose automatic lifecycle, retries, and no-code blocks will keep using the old context.
  * ⚠️ Leave `makeMain` at **`true`** unless you’re deliberately running a self-managed context for an advanced use case. If you do, see [**Adopt Self-Managed Contexts**](#id-8.-adopt-self-managed-contexts-advanced) for more details further below.
  * 💡`makeMain: false` is ignored if you launch a context with a sticky profile that is already launched and has already been made main elsewhere. This is because launches of the same sticky browser profile all share the same browser instance, see [**Sticky Profiles**](#id-5.-persistent-sticky-browser-profiles-clearprofile-cloneprofile) for more details further below.
* **`setAsDefaults` (default: `true` if `makeMain` is `true`, otherwise `false`)** — *advanced*\
  Sets these options for future launches as well as automatic relaunches (e.g., after a staleness or crash recovery) for the rest of the TaskBot run.
  * ⚠️ If you set `setAsDefaults` to **`false`**, a later relaunch may revert to older defaults. Likewise, if you set `setAsDefaults` to **`true`** while `makeMain` is **`false`**, the main context may relaunch with unexpected, unrelated settings. Prefer keeping the default.&#x20;

***

### 3. Defaults — `setDefaults()`, `getDefaults()`, `resetDefaults()`

Defaults are the settings a TaskBot uses when launching or relaunching contexts. You can ensure that any subsequent launch or relaunch uses the settings you want by setting defaults.

See [**Core Concepts**](#id-1.-core-concepts) → **Context (re)launch** for a list of cases when a browser context is (re)launched.

#### At a Glance

* <mark style="color:$info;">async</mark>\
  `await zw.browserContext.getDefaults()` \
  → returns [`CustomLaunchArgs`](#launch-arguments-customlaunchargs).
* <mark style="color:$info;">async</mark>\
  `await zw.browserContext.setDefaults(args:` [`CustomLaunchArgs`](#launch-arguments-customlaunchargs)`)` \
  → updates the TaskBot-level default launch settings for the current run.
* <mark style="color:$info;">async</mark>\
  `await zw.browserContext.resetDefaults()` \
  → resets the TaskBot-level default launch settings to what’s defined in **Browser Launch Settings**.

**Notes**

* `zw.browserContext.getDefaults()` excludes `policy`, which is only meaningful when launching or setting defaults.
* In `setDefaults()`, only `policy.inheritDefaults` is accepted (`makeMain` and `setAsDefaults` are ignored — they have no effect when setting defaults).

#### **Examples**

**Set all subsequent (re)launches to headless**

```js
// @zw-run-locally
await zw.browserContext.setDefaults({
  launchConfig: {
    launchOptions: { headless: true },
  }
});

await zw.browserContext.launch(); // now guaranteed to launch headless
```

**Discover a sticky profile’s ID**

```js
const defaults = await zw.browserContext.getDefaults();
const stickyProfileId = defaults?.launchConfig?.mode === "sticky"
  ? defaults.launchConfig.stickyProfileId
  : null;

await zw.log("Sticky profile ID:", stickyProfileId);
```

***

### 4. Context — `getContextInfo()`, `getContext()`

#### At a Glance

* `zw.browserContext.getContext()` \
  → returns the current main browser context.
* <mark style="color:$info;">async/sync\*</mark>\
  `await zw.browserContext.getContextInfo()` → returns:

  ```js
  {
    id: string,
    contextProps: {
      mode: "sticky" | "incognito",
      stickyProfileId?: number, // when mode="sticky"
      maximize?: boolean,
      headless?: boolean,
      bypassDetection?: boolean,
      proxy?: string,           // proxy server, example: '102.242.95.95:6407'
      foreignContext?: boolean, // advanced: true if launched with contextProvider
    },
    usedInTaskbots: number[],   // TaskBots currently using this context (can be multiple when mode="sticky")
    runConfig: {
      keepAlive?: boolean,
      bringToFront?: boolean,
    },
  } | null
  ```

\*`async` in browser, `sync` locally (see [Broken link](https://docs.zerowork.io/using-zerowork/using-building-blocks/write-javascript/broken-reference "mention")).

**Working with the returned context**

You can call any Playwright `BrowserContext` API. For the full list of available methods, properties and events, see [**here**](https://playwright.dev/docs/api/class-browsercontext).&#x20;

> **⚠️ Avoid** adding cookies, scripts, and permissions via the context API (e.g., `context.addCookies()`). Instead, pass them to [`zw.browserContext.launch()`](#id-2.-launch-the-browser-launch) or [`zw.browserContext.setDefaults()`](#id-3.-defaults-setdefaults-getdefaults) (e.g., `zw.browserContext.launch({ launchConfig: { cookies: [] } })`) so that they reapply on relaunch.

> ⚠️ **Avoid** changing user agent, timezone, locale, or geolocation (e.g., `context.setGeolocation()`), as this can harm the built-in anti-detection measures.

#### **Examples**

**Add a listener**

```js
// @zw-run-locally
const context = zw.browserContext.getContext();
context.on("close", async () => {
  await zw.log("Context is being closed.");
});
```

**Clear cookies**

```javascript
// @zw-run-locally
const context = zw.browserContext.getContext();
await context.clearCookies();
```

**Relaunch non-headless if the current context is headless**

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

const contextInfo = await zw.browserContext.getContextInfo();

if (contextInfo?.contextProps?.headless) {
  // Relaunch because a headful browser is required
  await zw.browserContext.quit();
  await zw.browserContext.launch({
    launchConfig: {
      launchOptions: { headless: false },
    }
  });
  await zw.log("Browser is relaunched.");
  
  const updatedInfo = await zw.browserContext.getContextInfo();
  await zw.log("Confirm headless is now false", updatedInfo.contextProps.headless);
}
```

***

### 5. Sticky Browser Profiles — `clearProfile()`, `cloneProfile()`, `listProfiles()`

Sticky profiles let multiple TaskBots share one browser session (same cookies/storage/fingerprint) via a persistent, shared user data directory.

#### At a Glance

* <mark style="color:$info;">async</mark>\
  `await zw.browserContext.clearProfile({ stickyProfileId: number })` \
  → clears the profile, or refuses to clear if the profile is in use.
* <mark style="color:$info;">async</mark>\
  `await zw.browserContext.cloneProfile({ cloneTo: { stickyProfileId: number }, cloneFrom: { profilePath: string } })` \
  → clones the profile, or refuses to clone if the **target** profile is in use. You can find the profile path by opening `chrome://version` in your browser and copying the **Profile Path** value.
* <mark style="color:$info;">async</mark>\
  `await zw.browserContext.listProfiles()` \
  → lists available profiles.

#### Behavior

* **Parallel TaskBots, one instance:** One shared browser instance (with multiple isolated tabs) per `stickyProfileId`.
* **In-use lock:** If a profile is in use, both `clearProfile` and `cloneProfile` will fail to avoid breaking active runs.
* **Create and discover:** You can create sticky profiles in **Browser Launch Settings** at creator.zerowork.io. You can’t create them via the API, but you can:
  * Create one in the UI (**Browser Launch Settings**).
  * Click **Save** to create the profile. Then open the profile again — the **Profile ID** appears at the bottom left. Copy it from there, or run `zw.browserContext.listProfiles()` to discover it.
  * Use that profile ID as `stickyProfileId` programmatically.
* **`contextProvider` and sticky mode:** `contextProvider` isn’t supported in sticky mode. If `mode` is `"sticky"` **and** `contextProvider` is provided, ZeroWork throws an error.

{% hint style="warning" %}
If a sticky profile still uses the legacy profile setup from the former **Non-incognito run mode** or **Run in my regular browser** option, `cloneProfile()` and `clearProfile()` are not supported. These methods do not throw an error, but they have no effect.
{% endhint %}

#### Shared Browser Side Effects

* **Non-deterministic tab order** across independent TaskBots. If you need to switch programmatically, match by URL and/or TaskBot ID.\
  **Example**

  ```js
  // @zw-run-locally
  const TASKBOT_ID = 1243;

  const pagesForThisBot = zw.browserContext
      .listPages()
      .filter(p => p.usedBy === TASKBOT_ID);
  ```
* **Attach semantics —** when a browser instance is already live for that profile ID and a launch event attaches to it, browser-level arguments are **ignored** and others are **applied**. By default, applied items affect the whole context, so other TaskBots sharing the instance will see them (except `keepAlive` and `bringToFront`, which apply to this run’s tabs only).\
  \
  **Ignored on attach:**

  * `launchConfig.launchOptions`
  * `launchConfig.contextOptions`
  * `launchConfig.bypassDetection`, `launchConfig.maximize`

  **Applied:**

  * `runConfig` — applies to this run's tabs only.
  * `launchConfig.scripts` — scripts that already ran are ignored; any **new** scripts are applied (context-wide).
  * `launchConfig.cookies` — duplicate cookies by the same `name` + `domain` + `path` are ignored; any **new** cookies are applied (context-wide).
    * ⚠️ Be careful not to pass conflicting cookies or two distinct cookies with the same `name`/`domain`/`path` (duplicates will be ignored).
  * `launchConfig.onContextReady` — runs on **every** attach (gate it if you want it to run only on true (re)launches).\
    **Gate example:**

    ```javascript
    // @zw-run-locally
    await zw.browserContext.launch({
      launchConfig: {
        mode: "sticky",
        stickyProfileId: 6567,
        onContextReady: async (context) => {
          // Runs on every attach; gate if you only want true (re)launches
          if (context._onContextReadyRan === true) {
            await zw.log("Attach detected — skipping onContextReady.");
            return;
          }
          await zw.log("Running onContextReady on a true (re)launch only.");
          context._onContextReadyRan = true;
        },
      },
    });
    ```

#### Anti-patterns

* **`keepAlive` caution.**\
  We don’t recommend setting `runConfig.keepAlive` to `true` in `zw.browserContext.launch()` or `zw.browserContext.setDefaults()` **when using sticky profiles**. After a long device sleep, the connection can drop while the browser stays open, blocking the profile until you restart the Desktop Agent or quit that browser. Only one browser instance can use a profile at a time. Prefer leaving `runConfig.keepAlive: false` for sticky profiles.

***

### 6. Quit Browser — `quit()`

#### At a Glance

* <mark style="color:$info;">async</mark>\
  `await zw.browserContext.quit(opts?: { forceQuit?: boolean })` \
  → **closes** the current context and the browser instance.

#### **Behavior**

* Closes the current run’s pages and attempts to close the browser/context.
* Since `zw.browserContext.quit()` is called explicitly, the `keepAlive` setting is ignored.
* In sticky profiles:
  * If other TaskBots are still using the same instance, pages from this run close, but the browser instance does not quit.
  * If no other TaskBots are using the instance, the browser quits fully.
  * `forceQuit: true` forces termination even if mode is `"sticky"` and browser is actively shared. Use with care.

> 💡Contexts are automatically managed and closed when needed. See [**Core Concepts**](#id-1.-core-concepts) → **Lifecycle**. You only need to call `zw.browserContext.quit()` if it's part of your logic. Otherwise, lifecycle management is handled out of the box.

***

### 7. Active Page & Pages — `setActivePage()`, `getActivePage()`, `isActivePage()`, `createPage()`, `listPages()`

#### At a Glance

* <mark style="color:$info;">async</mark>\
  `await zw.browserContext.setActivePage(page: Page, options?: { forceContextMismatch?: boolean })` \
  → makes a Page the “active page” used by no-code building blocks that do web interactions.
* `zw.browserContext.getActivePage()` \
  → returns `Page | null`, i.e. the page (if any) currently used by the TaskBot and its no-code building blocks.
* `zw.browserContext.isActivePage(page: Page)` → returns `boolean`
* <mark style="color:$info;">async</mark>\
  `await zw.browserContext.createPage({ url?: string, context?: BrowserContext })` → creates a new page and returns `Page`
* `zw.browserContext.listPages()` → returns:

  ```typescript
  Array<{
    page: Page,
    url: string,
    isActive: boolean,
    contextId: string,
    usedBy: number, // TaskBot ID
  }>
  ```

**What’s an active page?**

The **active page** is the page that no-code web-interaction blocks act on. There can be only one active page at a time. You can open other pages in code, but unless you set one as active, no-code blocks won’t use it.

**Example**

```js
// @zw-run-locally
const page = await zw.browserContext.createPage({ url: "https://wikipedia.org" });
await zw.browserContext.setActivePage(page);
```

#### **Notes & caveats**

**`createPage` — create in custom context** — *advanced*

By default, pages are created in the current main context. If you need to create a page in a self-managed, non-main context, you can pass it in `context`, and the page will be created there instead.

```javascript
const page = await zw.browserContext.createPage({ context: myCustomContext });
```

**Context mismatch & `forceContextMismatch`**

If the page belongs to a different browser context than the current main context, an error is thrown similar to:

> The page you provided belongs to a different browser context than the current main context. If you must use a custom launch flow, provide `contextProvider` in `zw.browserContext.launch()`. If you must use a self-managed context, use `zw.browserContext.adoptContext()` to adopt it before calling `zw.browserContext.setActivePage()`. While not recommended, you can also set `forceContextMismatch` to `true`.

* **`forceContextMismatch` (default: `false`)** — *advanced*
  * Consider it an **escape hatch**. Setting to `true` is not recommended.&#x20;
  * If set to `true`, the page is made active even if it belongs to a foreign context. No-code blocks will now operate on the active page, but inconsistencies can arise when Switch or Close Tabs is used, when a context **relaunches**, or when you use [`zw.browserContext.getContext()`](#id-4.-context-getcontextinfo-getcontext).&#x20;
  * If you must launch a custom context, explore [`contextProvider`](#context-provider-contextprovider-advanced) in `zw.browserContext.launch()` first. For more advanced use cases and self-managed contexts, explore [`zw.browserContext.adoptContext()`](#id-8.-adopt-self-managed-contexts-advanced).&#x20;

**Two ways to hit a mismatch**

1. Launching a second context with `makeMain: false` and creating a page there.
2. Creating a context entirely outside `zw.browserContext.launch()` (e.g., using the Playwright `BrowserType` API).

**Bad (illustrative) pattern**

```js
// 🚫 Bad (illustrative) pattern

// Launch main context
const context1 = await zw.browserContext.launch();

// Launch a second independent context
const context2 = await zw.browserContext.launch({
  // 🚫 Bad: makeMain is false → launched a loose, non-managed context
  policy: { makeMain: false } 
});
const page = await context2.newPage();

// This will throw unless forceContextMismatch is set to true (not recommended)
await zw.browserContext.setActivePage(page);
```

***

### 8. Adopt Self-Managed Contexts (Advanced)

For advanced use cases, you can create additional (non-main), self-managed contexts via `zw.browserContext.launch()` and then let ZeroWork adopt one of them as the active, managed context.

**At a Glance**

* <mark style="color:$info;">async</mark>\
  `await zw.browserContext.adoptContext(context: BrowserContext)` \
  → adopts a non-main context **launched by `zw.browserContext.launch()`** (with `policy.makeMain: false`).

> ⚠️ Contexts created directly via the Playwright `BrowserType` API are **not** accepted. If you need a custom launch flow, provide it via [`contextProvider`](#context-provider-contextprovider-advanced) in `zw.browserContext.launch()`.

**Example**

**Switching between two launched (non-main) contexts**

```js
// @zw-run-locally

// Launch two independent contexts (illustrative; only do this if you truly need it)
const LINKEDIN_ACCOUNT_A = [/* cookies */];
const PROXY_USA = { server: "socks5://..." };

const LINKEDIN_ACCOUNT_B = [/* cookies */];
const PROXY_ITALY = { server: "socks5://..." };

const launchArgsA = {
  launchConfig: {
    cookies: LINKEDIN_ACCOUNT_A,
    launchOptions: { proxy: PROXY_USA },
  },
  runConfig: { keepAlive: true },
  policy: { makeMain: false }, // create as non-main
};
const contextA = await zw.browserContext.launch(launchArgsA);
const pageA = await zw.browserContext.createPage({ context: contextA });

const launchArgsB = {
  launchConfig: {
    cookies: LINKEDIN_ACCOUNT_B,
    launchOptions: { proxy: PROXY_ITALY },
  },
  runConfig: { keepAlive: true },
  policy: { makeMain: false }, // create as non-main
};
const contextB = await zw.browserContext.launch(launchArgsB);
const pageB = await zw.browserContext.createPage({ context: contextB });

// Persist references across blocks and TaskBots (same Desktop Agent)
const globalState = zw.globalState.access();
globalState.contexts = {
  A: { context: contextA, page: pageA, defaults: launchArgsA },
  B: { context: contextB, page: pageB, defaults: launchArgsB },
};

const adoptSafely = async ({ context, page, defaults }) => {
  // Adopt the context into the managed flow
  await zw.browserContext.adoptContext(context);

  // Ensure continuity on relaunches
  await zw.browserContext.setDefaults(defaults);

  // Point no-code blocks to the right page
  await zw.browserContext.setActivePage(page);
};

// Persist helper so it can be called in other TaskBots/blocks
globalState.adoptSafely = adoptSafely;

// Later in another block or TaskBot: adopt and switch to A
await globalState.adoptSafely(globalState.contexts.A);

// Later in another block or TaskBot: adopt and switch to B
await globalState.adoptSafely(globalState.contexts.B);
```

***

### Notes

* **Largely unavailable in browser execution.**\
  The `zw.browserContext.*` API is available mostly for local code execution. Only inspection methods `zw.browserContext.getContextInfo()` and `zw.browserContext.getDefaults()` are supported in the browser.&#x20;
