# Home and Welcome

ZeroWork is dedicated to creators who want to build TaskBots that automate repetitive tasks such as typing, clicking and copy-pasting data in the browser.&#x20;

TaskBots are designed to mimic human interactions on any website by clicking buttons, saving text, and more. They act like real human users! This enables automation without limits. 🚀

No coding required.

### Typical Use Cases

* **Scrape**: Collect LinkedIn profiles, FB group members, IG followers, Amazon products.
* **Enrich data**: Enrich collected data by collecting data from individual pages such as email, job title, rating, reviews, etc.
* **Automate actions**: Auto-send DMs, auto-like content, auto-follow, auto-fill out forms.
* **Transform data** **(calculate, filter, compare)**: Auto-qualify your leads based on keywords, location or activity, auto-format or tag collected data.
* **Add AI**: Auto-post AI-generated blog posts, auto-comment with AI-generated comments, auto-reply to messages with AI, auto-send AI-generated product or prospect summaries to your email.

### Getting Started

* [Sign Up](/getting-started)
* [Install the Agent](/install-the-agent)
* [Crash Course](/crash-course)
* [FAQs](https://zerowork.io/faq)

### TaskBot Creation Process&#x20;

* [Running TaskBots](/using-zerowork/general-run-schedule-share-webhooks)
* [Using Building Blocks](/using-zerowork/using-building-blocks)
* [Using Selectors](/using-zerowork/using-selectors)
* [Using Tables](/using-zerowork/using-tables)
* [Using Variables](/using-zerowork/using-variables)

### Troubleshooting and Support

* [Common Problems](/using-zerowork/common-problems)
* [TaskBot Creator Support](/support/getting-support)


# Getting Started

Start your 14-day free trial [here](https://creator.zerowork.io/register) — no credit card needed.

To get set up:

1. **Install the desktop agent**\
   Download the ZeroWork agent from [this link](https://creator.zerowork.io/download-agent) and follow this [quick guide](/install-the-agent) to install it.
2. **Chrome must be installed (even if you don't use it)**\
   TaskBots run in an automated Chrome browser behind the scenes. You can build and start them from any browser, but Chrome needs to be installed on your device for the automation to work.\
   Also note: if you're using a browser with strict security settings (like Brave), you may need to whitelist the ZeroWork agent in your browser settings.
3. **Watch the crash course**\
   Our [crash course](/crash-course) walks you through building and running your first TaskBot.


# Install the Agent

Agent is needed to run your TaskBots on your computer.

{% hint style="danger" %}
Agent does not have any user interface, it simply 'sits' on your desktop to run TaskBots. The actual building of TaskBots is done at <https://creator.zerowork.io/workflows>.
{% endhint %}

### Step 1: Download

Download the agent for Mac, Windows or Linux from <https://creator.zerowork.io/download-agent>.

### Step 2: Install

#### **On Mac**

Move the agent icon to the Applications folder.

<figure><img src="/files/eseyXWhft2BdGMfEJThs" alt=""><figcaption></figcaption></figure>

#### **On Windows**

Click on the downloaded .exe file to initiate installation process. If prompted for permissions, confirm and grant access.

**On Linux**

Select the type you need for your Linux distribution: .AppImage, .deb or .rpm. For Ubuntu or Mint, choose .deb. For Fedora, choose .rpm. Use .AppImage if your distribution doesn’t support .deb or .rpm.

**Notes about Linux installation:**

* If you choose to use the .AppImage format, it is recommended to install the AppImageLauncher application (<https://github.com/TheAssassin/AppImageLauncher>) and use it to add/remove the agent.
* If after starting the agent and waiting for at least 30 seconds, there is no agent tray icon visible in your toolbar / taskbar, please first make sure the agent is running by opening [http://localhost:9990](http://localhost:9990/) in your browser. If you see the agent-is-running message as a response, then the issue is likely that the tray icon is missing due to not having the appindicator extension installed. Fix it by installing  the "extension manager" app, start it and search for extension named "appindicator ..." and install it. For more details, see: <https://linuxiac.com/how-to-enable-system-tray-icons-in-gnome/>.
* The openSUSE distribution is not supported.

### Step 3: Double-Click to Activate the Agent

Once installed, double-click the agent to activate it. Double-clicking the agent will make it appear in your toolbar, as shown below (top-right on Mac and bottom-right on Windows).&#x20;

If you see a security prompt by your operating system, such as "ZeroWork Agent is an app downloaded from the internet. Are you sure you want to open it?", click confirm.

You won’t see any user interface when the agent runs — this is intentional. The agent works quietly in the background, and that's exactly how it’s meant to function. To build and start TaskBots, go to [https://creator.zerowork.io](https://creator.zerowork.io/).

#### Mac view

<figure><img src="/files/NuaAgBxjMOUCElCOWSgg" alt=""><figcaption></figcaption></figure>

#### Windows view

<figure><img src="/files/jQgZn1Bg456B5I7EAkNa" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/HXwRVEhcvWi80g7aMaow" alt=""><figcaption></figcaption></figure>

### Step 4: Confirm Successful Installation

Build a simple TaskBot that opens a link, as described here [Open Link](/using-zerowork/using-building-blocks/open-link). (You can open any link. For example, just use a wikipedia page <https://www.wikipedia.org/>.) This is meant as a test to confirm that the agent is working.

If you see a browser window popping up, opening that link and then quickly closing the browser window again, then your installation was successful! 🥳

{% hint style="warning" %}
On Windows, the browser window where the TaskBot runs might not get in front. It might stay behind your primary window. Simply toggle [Stay on Page after Run](/using-zerowork/general-run-schedule-share-webhooks/run-settings/stay-on-page-after-run) or minimize your windows after the TaskBot finishes running to locate the Chrome window.
{% endhint %}

### How to Use Agent Options

You can click on the agent in your toolbar to see its options.&#x20;

In the options, you can see the version of your agent and its status (should be 'ready').

**Mac view**

<figure><img src="/files/8RoUR2yCC0aKUsmgCnhQ" alt=""><figcaption></figcaption></figure>

**Windows view**

<figure><img src="/files/vfGd3o6F0seDvoZUJAaF" alt=""><figcaption></figcaption></figure>

### Video Tutorial: Install ZeroWork Agent

{% embed url="<https://youtu.be/95hvDqphZc8>" %}


# Crash Course

Ready to build your first TaskBot? This crash course covers all the basics. 🚀

{% embed url="<https://www.youtube.com/playlist?list=PL7X7ThrPNPJAN8nfHlfguop5ylcZyJjsw>" %}


# General: Run, Schedule, Share, Webhooks


# Run

When you click on *Run,* your TaskBot will open a new Chrome browser window and execute the task. To see how *Stop* works, see [Stop](/using-zerowork/general-run-schedule-share-webhooks/stop).

You can click *Run* inside the workflow, as shown below:

<figure><img src="/files/yR5HOJz1OHQztT0Tfr3y" alt=""><figcaption></figcaption></figure>

Or, you can click on the Play button from your TaskBot list, as shown below:

<figure><img src="/files/bH5fOp6vQs17ok1iq1aF" alt=""><figcaption></figcaption></figure>

Each time you start running a TaskBot, a real-time display of its actions will be shown:

<figure><img src="/files/42Gua3EJnVRonexzr2Wv" alt=""><figcaption></figcaption></figure>

### FAQ

#### How many TaskBots can I run in parallel at the same time?

As many as your computer resources allow - that would be between 5 to 20 on a regular Mac depending on what your TaskBot set-up is.

#### Troubleshooting: TaskBot does not run

Please see [My TaskBot Does Not Start Run](/using-zerowork/common-problems/my-taskbot-does-not-start-run).


# Stop

When TaskBot started its run, you can stop it at any time by clicking on the *Stop* butto&#x6E;*.*

<figure><img src="/files/IhN6VwWCqXI7rdrsNsAD" alt=""><figcaption></figcaption></figure>

{% hint style="success" %}
The data that the TaskBot collected or manipulated up until the stop signal will be saved.
{% endhint %}


# Log in to the Agent

When using webhooks or using scheduler, you need to log in to your agent, as shown below. You do not need to log in to your agent if you don't use webhooks or scheduler.&#x20;

This needs to be done **only once** after the installation.

You don't need to be logged in to your agent if you only run TaskBots manually.

#### Mac view

![](/files/odrBvLz2rBklaFdtnNOC)

#### Windows view

<figure><img src="/files/KKNpObx6VQ1mdXhyGo1g" alt=""><figcaption><p>Bottom right view</p></figcaption></figure>

<figure><img src="/files/zRlyEnMigbeT1yvlMUyK" alt=""><figcaption><p>Click on ZeroWork agent to see the options</p></figcaption></figure>

#### After you logged in

After you logged in to your agent, you should see status **connected**, as shown below. Same is the case for webhooks if your account has any active webhooks.

![](/files/1CRiCtIHImqXI0f7U7E6)


# Run Settings


# Run in Background

When enabled, TaskBot will run in the background, hiding the browser window entirely.

When disabled, TaskBot will display the browser window on your screen, allowing you to watch its actions.

To enable this option, click on the settings gear icon in the top-right corner of your TaskBot page, navigate to TaskBot settings, and select this option.

<figure><img src="/files/9pE9TkiXNU4EiDilT5gJ" alt=""><figcaption></figcaption></figure>

### FAQ

**When should I use this option?**

* **During Building or Testing:**\
  Keep this option disabled while building or testing your TaskBot so you can monitor its actions and ensure everything runs as expected.
* **After Ensuring Reliability:**\
  Once you’re confident your TaskBot is functioning reliably, you can enable this option. Here are some additional tips:
  * Many users with two or more monitors prefer to leave *Run in Background* disabled. This allows them to keep an eye on the TaskBot's progress in another browser window on their secondary monitor while continuing their work on their primary monitor.
  * Other users prefer to keep it enabled at all times, monitoring TaskBot activity via the **Running TaskBots display**, reviewing logs and setting up notifications for failed runs (see [Using Run Reports](/using-zerowork/using-run-reports)).


# Stay on Page after Run

When enabled, TaskBot will keep the browser window open after completing its run. This is particularly useful if you want to remain on the page to perform manual actions after the TaskBot finishes.

When disabled, TaskBot will close the window automatically.&#x20;

To enable this option, click on the settings gear icon in the top-right corner of your TaskBot page, navigate to TaskBot settings, and select this option.

<figure><img src="/files/3KLPnkTt5wyMmDVbodAL" alt=""><figcaption></figcaption></figure>

### FAQ

**When should I use this option?**

Keep disabled to save computer resources, unless you need to verify something on the page after the run (e.g., check if a form was filled correctly) or perform manual actions after the TaskBot finishes.


# Bring Pages to Front

This setting brings each new active page (such as a new tab or a link opened in a new tab) to the front.

{% hint style="info" %}
Some operating systems, like macOS, will bring the browser window to the front *once* at the start of the run when launching Chrome, regardless of whether this option is enabled or disabled.
{% endhint %}

When this option is disabled, your TaskBot does not bring new tabs to the foreground when opening or switching to them. If you observe your TaskBot in action and it uses the [Switch or Close Tab](/using-zerowork/using-building-blocks/switch-or-close-tab) building block, you’ll notice that the TaskBot switches to the new tab silently, without bringing it to the foreground.

To disable or enable this option, click on the settings gear icon in the top-right corner of your TaskBot page, navigate to TaskBot settings, and select this option.

<figure><img src="/files/rDJElyVSGOouPbZG3v7q" alt=""><figcaption></figcaption></figure>

### FAQ

**When should I use this option?**

Keep enabled while building or testing your TaskBot, so you can monitor pages as they come to the front during the run.


# Concurrent Runs

<figure><img src="/files/okILudu5eTkTdPR7W5lL" alt=""><figcaption></figcaption></figure>

{% hint style="success" %}
**Available in** [**Agent 1.1.75**](/release-notes/version-1.1.75) **and higher**
{% endhint %}

**Concurrent runs**, also called **parallel runs**, allow the same TaskBot to run more than once at the same time on the same Desktop Agent. This is useful when a TaskBot handles independent jobs that shouldn't wait on each other, such as starting a separate run for each incoming webhook call.

{% hint style="info" %}
**What's a concurrent / parallel run?**\
Two or more runs of the *same* TaskBot that are in progress simultaneously on the same Desktop Agent. Each run works independently and is unaware of the others; they simply overlap each other on the same machine.
{% endhint %}

{% hint style="success" %}
Running *different* TaskBots at the same time is a separate concept and is enabled by default, without needing the Concurrent Runs setting.
{% endhint %}

### How To Use It

Concurrency is configured per TaskBot, and is turned off by default. To enable it:

* Open your TaskBot's canvas.
* Click **More options** (the three dots) in the top right.
* Click **Concurrent runs**.
* Check the checkbox in the modal that appears.

<figure><img src="/files/vdQXx3D0NTz3zFhmnqO7" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
Your Desktop Agent must be linked (logged in) for concurrent runs to work. Concurrent runs aren't meant to work on a Guest Agent, which is an Agent you haven't logged in to. A Guest Agent can only perform manual TaskBot runs, so you'll see an error if you trigger a second run for the same TaskBot while one is already in progress when using an unlinked Agent.
{% endhint %}

### Things To Know

* **This setting is only about one TaskBot running in parallel on the same Agent.** *Different* TaskBots have always been able to run at the same time on one Agent, with or without this setting. Concurrent runs decide whether the *same* TaskBot can have more than one run in progress.
* **Your hardware is the practical limit.** ZeroWork does not cap how many runs of a TaskBot happen in parallel. What your machine can handle depends entirely on what your TaskBot does; a browser-heavy TaskBot typically exhausts resources far sooner than one that only moves data on the local machine.
* **Persisted variables are shared between runs.** Every concurrent run reads and writes the same variable values, so the last run to write a value wins. When you don't want that, turn off **Persist value** in the settings icon beside the variable. A non-persisted variable belongs to each run individually, so overlapping runs can't overwrite each other's variables. See [Variables.](/using-zerowork/using-variables)
* **Native tables and Google Sheets can be shared too.** Parallel runs read, append, and update the same rows. This was already true of any table or sheet shared between TaskBots; concurrency doesn't change this.
* **Device storage is shared by every run.** Device storage keys belong to the Agent, not to a run, so all concurrent TaskBot runs on that Agent see the same keys and values.
* **Both Default and Additional Agents support it.** Concurrency requires a linked Agent, which means your account's Default Agent or any Additional Agent you've purchased and linked with its API key will work with this setting.
* **The** [**Run TaskBot block** ](/using-zerowork/using-building-blocks/run-taskbot)**works alongside concurrency.** A TaskBot started by another TaskBot follows the same rules: if the called child TaskBot has concurrent runs enabled, several parent TaskBots can run it in parallel. Manually stopping a parent run also stops the child runs it started with **Wait until the TaskBot finishes** enabled.

### Run Behavior

* **When concurrent runs are enabled** and another run is triggered, both runs execute in parallel and independently of each other.
* **When concurrent runs are disabled** and another run is triggered while one is in progress, the second run is rejected with an error and does not start. The run already in progress continues.
* **When a schedule fires while a run of that TaskBot is in progress**, the scheduled run starts and overlaps the existing in-progress run. With concurrent runs disabled, the scheduled run is skipped and the run already in progress continues.
* **When the Agent is a Guest Agent**, concurrent runs do not work regardless of this setting. Scheduled and webhook-triggered runs don't run on a Guest Agent either, so the only runs available are manually triggered ones, and a second manual run is rejected while the first run is in-progress. Log in to the Agent to use concurrent runs.
* **While any run is in-progress**, a green badge beside the run button at the top of the TaskBot canvas shows how many runs are in progress, such as **2 running**. It appears for a single run as well as for concurrent ones. Click the badge to expand it: each active run is listed with how it was started, the time it started, and a link to open its run details.
* **When you stop one run from the badge**, only that run stops. Each listed run has its own stop button; the other runs continue.

<figure><img src="/files/ucBy6gLQPaZNlcJyAGwx" alt=""><figcaption></figcaption></figure>

### FAQ

1. **What can start concurrent runs?**<br>

   Concurrency is about *overlapping or running the same TaskBot in parallel*, not about how each run was started. Any trigger can start a run, and a run from any trigger can run in parallel with a run from any other:

   * **Manual runs.** Click **Run** while a run is already in progress.
   * [**Webhook-triggered runs**](/using-zerowork/general-run-schedule-share-webhooks/trigger-run-via-webhook)**.** Each call starts its own run.
   * [**Scheduled runs**](/using-zerowork/general-run-schedule-share-webhooks/scheduler)**.** A scheduled run is a run like any other. It starts even when a run of that TaskBot is already in progress, and likewise, while a scheduled run is in progress, triggering the same TaskBot manually or by webhook starts a second run in parallel with it.

{% hint style="warning" %}
Enabling concurrent runs changes how your existing scheduled run behaves. Prior to `1.1.75`, a scheduled run was skipped if the TaskBot was already running, and the active run simply continued. With concurrent runs enabled, the scheduled run starts anyway and overlaps the run already in progress. If your TaskBot can take longer than its schedule interval, it will now overlap itself.
{% endhint %}


# TaskBot Sharing Options

Click on *Share TaskBot*  on your TaskBot builder page.

<figure><img src="/files/rG0FcRRFJ77eNTzBEu9b" alt=""><figcaption></figcaption></figure>

There are two options how you can share your TaskBot.

Whether you use the option to share with specific user or generate a sharing link, a full copy of your TaskBot, including its parameters, variables and tables, will be added to a receiving user's ZeroWork account. Your specific data will not be shared, which means that the receiving user's ZeroWork native tables will have empty rows and variables will have empty values. Google Sheet tables will still contain your Google Sheet link, so be sure to restrict sharing access to it or change it before sharing, if this is not desired. Your TaskBot run settings (cookies, proxy, scheduler, etc.) will never be shared.

#### Option: Share with specific user

Enter the email address of the account you want to send TaskBot to. A copy of your TaskBot will be sent to that user's ZeroWork Creator account.&#x20;

<figure><img src="/files/vGFXb4Ksp0B3Meb2ApL0" alt=""><figcaption></figcaption></figure>

#### Option: Generate sharing link

You can activate a sharing link. Any user who you share this link with can pull a copy of your TaskBot to their ZeroWork Creator account.

By deactivating,  your link will become inaccessible.

By resetting (see the renew icon on the right of the link 🔄), your current link will become inaccessible and a new link will be generated.

<figure><img src="/files/KawOUZRWisdXkBtFVrcV" alt=""><figcaption></figcaption></figure>

### FAQ

#### Are cookies and proxy details shared?

No, your TaskBot run settings are never shared with the user who pulls a copy of your TaskBot. Their TaskBot run settings (including scheduler set-up) will be reset to blank or default values.

#### Are tables, variables and parameters sent to the other user, too?

When sending a TaskBot to another account, everything is duplicated including the TaskBot logic (building blocks), all the tables and their columns, variables and parameters.&#x20;

Data inside ZeroWork native tables and variables won't be duplicated in order to protect your privacy. This means that the duplicated TaskBot's variables will contain **empty** values and the native tables with have no rows.

Note that the Google Sheet tables will still contain your Google Sheet link. If this is not desired, be sure to change it to a different link or restrict access to it.

#### **What if I want to share the data inside my ZeroWork native tables, too?**

If you need to, you can export your data as CSV and give that CSV to the other user, so that they can [Import Data from CSV](/using-zerowork/using-tables/import-data-from-csv). The whole process takes just a few clicks on both sides.

#### After generating a sharing link, I added more changes. Will they be reflected?

Yes, the sharing link enables other users to duplicate your **current** TaskBot. So whatever changes you make, they are always reflected.&#x20;

#### **After sending to specific user, when will the user receive the TaskBot?**

It takes just a minute or less to duplicate the TaskBot to the other user. After you sent your TaskBot to another account, that user will see it in their list of TaskBots. They may need to reload their page to see the received TaskBot and its tables.

#### Troubleshooting: After **sending to specific user, t**he receiving user did not receive the TaskBot

Please ask them to reload their page.


# Cookies

Enable your TaskBot to access your browser cookie session, so that it doesn't have to log in every time it runs in an incognito session.

{% hint style="warning" %}
If you ever log out from the website that you copied cookies from, this will invalidate the cookie session. So if you log out, be sure to re-copy and re-paste your cookies again!
{% endhint %}

{% hint style="warning" %}
Your cookies may expire over time. Simply copy them again and paste to your TaskBot workflow, as described below.
{% endhint %}

### Step 1: Copy Cookies

Download this free-of-charge plugin Cookie-Editor: <https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm>.

Click on the puzzle sign on your Chrome bar and pin it, like shown below.

<figure><img src="/files/oEqoWuiPwE15UcWIBQlv" alt=""><figcaption></figcaption></figure>

Go to the website for which you need to get cookies. For example, if you are building a TaskBot for LinkedIn, open linkedin.com, make sure you are logged in, click on the Cookie-Editor plugin and click on Export button - marked red in the screenshot below. Then select *Export as JSON* option. That action will copy your cookies to your clipboard.&#x20;

<figure><img src="/files/rUXTwgVSecCQKqjbJGDf" alt=""><figcaption></figcaption></figure>

### Step 2: Paste Cookies to ZeroWork

Now return to your TaskBot builder page, open *TaskBot Settings* and paste your cookies in the cookies field. Now you are good to go!

<div align="center" data-full-width="false"><figure><img src="/files/Yp2HslHyJDuepba6XzdA" alt=""><figcaption></figcaption></figure></div>

### Troubleshooting: TaskBot Cannot Log in with Cookies

In some rare cases, some websites do not accept cookies to be logged in with.

In such cases, you have two options:&#x20;

* Use building blocks to build a login flow. For example, add the building block [Insert Text or Data](/using-zerowork/using-building-blocks/insert-text-or-data) with your username and password and then add the building block [Click Web Element](/using-zerowork/using-building-blocks/click-web-element) to click on the button "Log in". You can encrypt your password, as described here: [Insert Text or Data](/using-zerowork/using-building-blocks/insert-text-or-data#encrypt-content).&#x20;
* Use [Broken mention](broken://pages/tEjiJFmusryioJ0aoXpw) or [Broken mention](broken://pages/XOXx7oHkaE4nG0T85wbj) run modes.

### Video Tutorial

Start watching from 2:55 mark to see how to use cookies.

{% embed url="<https://youtu.be/zm11yRzEUgk?si=1wGLuLPm-cEd-1oA>" %}


# Scheduler

{% hint style="danger" %}
**IMPORTANT**:&#x20;

* When you use Scheduler for the first time, you need to **log in to your agent** (see instructions here: [Log in to the Agent](/using-zerowork/general-run-schedule-share-webhooks/log-in-to-the-agent)).
* During the scheduled time, your **agent must be active**. That means if you quit it or restarted your computer after which you didn't activate the agent, the scheduled task won't happen because there is no agent to run it.
* During the scheduled time, your computer **must be turned on and active**. You may want to adjust your sleep mode and battery settings to ensure that your computer doesn't turn off or switch to a sleep mode for the time that the TaskBot is scheduled to run.&#x20;
  {% endhint %}

### How to Set up the Scheduler

Click on the scheduler icon inside your TaskBot, as shown here:

<figure><img src="/files/d7OBfwlcE40okb1xQBxJ" alt=""><figcaption></figcaption></figure>

A dialog will open. Now you choose the time or interval for your TaskBot ton run.

<figure><img src="/files/XQXGzkfjPuOChcKOzeSE" alt=""><figcaption></figcaption></figure>

### FAQ

#### Can I schedule multiple TaskBots in parallel?

Yes, you can run parallel scheduled jobs - just make sure your computer has enough capacity to handle multiple parallel executions.

#### I want to run my scheduled TaskBots on a dedicated device but I want to keep building and running TaskBots manually on my current device without interfering with the scheduled TaskBots. How to achieve this?

Let's say, you have a dedicated computer A where you want to run your **scheduled** TaskBots, and you have computer B where you want to continue building and running TaskBots (in this case running means that you **manually** click on the run button).&#x20;

To enable this setup, follow these simple steps:

* Install the agent on both computers A and B.
* Log in to your scheduler inside the agent on your computer A (as described here: [Log in to the Agent](/using-zerowork/general-run-schedule-share-webhooks/log-in-to-the-agent)).
* Do **not** log in inside the agent on your computer B.&#x20;

Explanation: Your agent needs to be logged in **only** for scheduled runs and runs triggered by webhooks ([Trigger Run via Webhook](/using-zerowork/general-run-schedule-share-webhooks/trigger-run-via-webhook)). It does not need to be logged in for runs that happen when you click on *Run* button manually. So by logging in only to the agent that runs scheduled TaskBots (on computer A), you can ensure that the second agent (on computer B) won't pick up on any scheduled or webhook-triggered TaskBots and you can use it for manual runs.

#### I forgot to turn on my computer at the time of the scheduled run. Will it still run?

No, if the agent is inactive at the time when the scheduled run is supposed to start, then the scheduled run is skipped.

#### If a TaskBot is scheduled to run but it is currently running already, what will happen?

If a TaskBot is scheduled to run but it is already running, the new run will not be triggered. The currently active run will continue to run.

#### I tried logging into <https://creator.zerowork.io/> from device A (where I'm not logged in to my agent), and scheduling a TaskBot to run on device B (where I'm logged in to my agent). However, the scheduled run did not happen. What can be the issue?

You can schedule for a different device, but you would need to restart your agent on the device where it's supposed to run on schedule (in this case device B).

As a desktop app, the agent cannot be called by the backend that operates on the cloud. (A desktop application can call the backend itself, but it cannot be called by it.) This is why it needs to be called from frontend (in other words, your scheduling or rescheduling action in the user interface triggers the agent to update the list of its scheduled jobs).

However, when you restart the agent, it will fetch all the updated scheduled jobs.


# Trigger Run via Webhook

{% hint style="danger" %}

* Before you can use webhooks to trigger TaskBot run on your desktop agent, you need to log in to your agent, as described here: [Log in to the Agent](/using-zerowork/general-run-schedule-share-webhooks/log-in-to-the-agent).
* During the triggered run, your **agent must be active**. That means if you quit it or restarted your computer after which you didn't activate the agent, the run won't happen because there is no agent to run it.
* During the triggered run, your computer **must be turned on and active**. You may want to adjust your sleep mode and battery settings to ensure that your computer doesn't turn off or switch to a sleep mode for the time that the TaskBot is scheduled to run.&#x20;
  {% endhint %}

### Generate Webhook Link

Go to your TaskBot builder page, open *TaskBot Settings* and click on *Generate Webhook*.

<figure><img src="/files/TDmDGjsOAAKXsX7rsnXs" alt=""><figcaption></figcaption></figure>

Once generated, you will be able to copy the link and use it to trigger TaskBot run programmatically. You can use this link in third-party applications such as Zapier, Make, Pabbly, etc. The webhook can be triggered with a POST or GET method.

<figure><img src="/files/kzrboV61Z9MjLdWOl8Mr" alt=""><figcaption></figcaption></figure>

Beware that anyone with this link can trigger TaskBot run on your account. If you ever shared this link with anyone and now want to remove their access, you can either deactivate your webhook link or generate a new one.&#x20;

### Deactivate Webhook Link

Simply toggle *Webhook is active* option off. If the webhook is not active, it cannot be triggered.

### Generate a New Link

To generate a new link, delete your current one by clicking on the trash bin icon and then click on *Generate Webhook* button again. This action will generate a new link, and the old one will be permanently deleted.

### Send Dynamic Data

In a POST request, you can send dynamic data in the payload body. Query parameters are also supported (in both GET and POST requests).

#### In-built variable zw\_webhook\_data

Your data from the request body is always saved in the variable called zw\_webhook\_data. You do not need to create this variable manually. Whenever you trigger your TaskBot by a webhook, the variable will be updated or, if not created yet, automatically created.

<figure><img src="/files/8o3J3T1ag0nh0cEzOr0T" alt=""><figcaption></figcaption></figure>

The data is saved in stringified JSON format.

**Tip**: You can access nested objects or arrays that were saved to zw\_webhook\_data in [Write JavaScript](/using-zerowork/using-building-blocks/write-javascript) building block by using JSON.parse() method. Here is an example:

<figure><img src="/files/nTWvQ02NYTlFms3hqRA8" alt=""><figcaption></figcaption></figure>

#### Match specific values from your dynamic data to variables &#x20;

You can also create variables, and the data in your webhook payload will be matched to those. In addition to matching, the whole payload will *still* be saved to the aforementioned zw\_webhook\_data variable.

**Example**

Consider your TaskBot has these variables:

<figure><img src="/files/UxnlM12U0xpKrRrwsH7J" alt=""><figcaption></figcaption></figure>

You can add variable names and values as key-value pairs to your request body. For example, if you make your request via Postman (or any other API application), this is how it would look like:

<figure><img src="/files/PQVC0DQsWYEkzx9L5SuW" alt=""><figcaption></figcaption></figure>

After sending the webhook request as shown above, this would be the new state of your variables:

<figure><img src="/files/VDsOnqKzWYfzhGRtCQva" alt=""><figcaption></figcaption></figure>

#### Match nested objects or arrays to variables&#x20;

You can create variables as JSON paths to match nested objects or arrays (as well as execute JSON path supported queries on your payload).

**Example**

Consider this to be your webhook payload data:

```
{
    "city": {
        "name": "Berlin",
        "country": "Germany",
    }
}
```

This is how you can create a variable for one of the nested values:

<figure><img src="/files/y8KZJv8geTj4FxaySYMy" alt=""><figcaption></figcaption></figure>

#### Example of advanced JSON path use

JSON path operators (wildcard, filter, recursive descent) and query behavior are supported.&#x20;

Here is an example. Consider this to be your webhook payload data:

```
{
    "cities": [
        {
            "city": "Berlin",
            "population": 3577000,
            "country": {
                "name": "Germany",
                "identifier": "DE"
            }
        },
        {
            "city": "London",
            "population": 9748000,
            "country": {
                "name": "United Kingdom",
                "identifier": "UK"
            }
        },
        {
            "city": "New York",
            "population": 9469232,
            "country": {
                "name": "United States",
                "identifier": "US"
            }
        }
    ]
}
```

To get an array of all cities, you can use the wildcard operator \* as in `cities[*].city`.&#x20;

<figure><img src="/files/X2ZvsKwVup2FDTiLKNZj" alt=""><figcaption></figcaption></figure>

Similarly, you can use filter operators supported by JSON path to query specific data. Examples:

* Filter for the city object where city is Berlin: `cities[?(@.city==="Berlin")]`.&#x20;
* Filter for the cities with population over 5M: `cities[?(@.population>5000000)]`. This will return an array of city objects of those cities where population is over 5M.

#### Notes

* You do not need to include *all* (or any at all) variables in your body request. Include only those variables that you need to match data to.&#x20;
* Matching is case-sensitive. For example, "Link" will be matched to "Link" and will not be matched to "LINK" or "link".
* JSON path variable can be created with or without the dollar $ character. You can evaluate your JSON path here: <https://jsonpath.com/>. The resource can also help you validate your JSON path syntax.&#x20;
* If your JSON path is invalid or there is no match between some of the TaskBot variables and the webhook payload data, no error will be thrown.

### Trigger Webhook within a TaskBot Run

Sometimes you might want to trigger another TaskBot within a TaskBot run. For this, you can use *Send HTTP Request* building block (see [APIs: Send HTTP Request](/using-zerowork/using-building-blocks/apis-send-http-request)). See example set-up below.

<figure><img src="/files/AK9kvSu2035LL81A75uo" alt=""><figcaption></figcaption></figure>


# Proxies

Enable the TaskBot to be launched via your own proxies.

### Step 1: Open Your Proxy Provider's Dashboard

Enter your proxy address, username and password.

If your proxy does not require username and password, leave those fields blank.

For example, consider this proxy configuration provided by a commonly used proxy provider in the screenshot below.

<figure><img src="/files/g9hylS4Zm7Ut2lScjQm7" alt=""><figcaption></figcaption></figure>

Given this proxy configuration, then this will be the data you will need to enter.

* **Proxy address**: <mark style="background-color:red;">194.32.225.238:21261</mark> (proxy IP followed by the port, separated by a colon)
* **Username**: See username in 4th column
* **Password**: See password in 5th column

{% hint style="danger" %}
**IMPORTANT**: Your proxy address should be entered as a combination of ip **and** port, separated by a colon mark. \
\
**Example**: Proxy ip is 194.32.225.238 and the port is 21261.\
\
❌ 194.32.225.238 won't work because port is missing.\
❌ 194.32.225.23821261 won't work because port must be separated from ip.\
❌ 194.32.225.238.21261 won't work because port must be preceded by a colon mark, not dot.\
✅ **194.32.225.238:21261 will work.**
{% endhint %}

#### SOCKS5 Proxy

To use SOCKS5 proxies, use this format: socks5://\<your proxy>:3128, where 3128 corresponds to the port as described above.

### Step 2: Open Settings and Enter Your Proxy Data

Simply click on the settings gear on the top right of your TaskBot page to access TaskBot settings, click on *Add proxy* and then enter your proxy credentials.

<figure><img src="/files/2pbbgLVftgm0SO8CtD9S" alt=""><figcaption></figcaption></figure>


# Remote (Cloud) Execution

***Upcoming***

This option will be available at an additional pay-per-cloud-credit cost.&#x20;

With this option, you can run your TaskBots remotely on the cloud.


# How to Check and Update Your Agent

### How to Check If Your Agent Is Outdated

First, open your agent options (see how here:[Install the Agent](/install-the-agent#how-to-use-agent-options)) and check your currently installed agent version.

<figure><img src="/files/j4YLaoaF45GoXpSHqENx" alt=""><figcaption></figcaption></figure>

Then, open the general [**agent download page**](https://creator.zerowork.io/download-agent) and check what the latest available version is.

If the version displayed on the download page is higher than the version installed on your machine, your agent is outdated.

<figure><img src="/files/Q5RrtUmuJsq78tdafTvy" alt=""><figcaption></figcaption></figure>

***

### How to Update Your Agent

1. In your agent options, click **Check for update** and confirm the prompt to install the new version.
2. After the update, you'll be asked to confirm an agent restart — simply accept, and you're done!

<figure><img src="/files/JKQHOqcVsQT6GgeBAum3" alt=""><figcaption></figcaption></figure>

***

If for any reason the update fails, you can always uninstall your current agent and manually install the latest version from <https://creator.zerowork.io/download-agent>.

✅ Uninstalling and reinstalling the agent will not affect any of your scheduled TaskBots or webhooks.


# Install ZeroWork Agent on VPS

You can install and run the ZeroWork Agent on a VPS.

Follow this community tutorial to learn how:

{% embed url="<https://www.youtube.com/watch?v=_uhx_y_ZvGM>" %}


# Using Selectors


# What Are Selectors

Every element on a website can be identified by its unique selector (CSS or XPath). Each time you want your TaskBot to interact with an element of a website, such as clicking a button, your TaskBot needs to know the selector of that element in order to perform that action.

**Note**: While ZeroWork supports both CSS and XPath selectors, in this guide we will focus on CSS selectors only, as they are more beginner- and intermediate-friendly. We expect more advanced users to be already familiar with how to use XPath selectors. Here is a page whith some basic information about XPath: [How to Use XPath in ZeroWork (advanced)](/using-zerowork/using-selectors/how-to-use-xpath-in-zerowork-advanced).

Here is an overview of how to get selectors:

* [Copy Selectors](/using-zerowork/using-selectors/copy-selectors)
* [Use Element's Text](/using-zerowork/using-selectors/use-elements-text)
* [How to Build Custom Selectors](/using-zerowork/using-selectors/how-to-build-custom-selectors)

Are you wondering which way is the best? In the beginning, it may be enough to use the first two methods. These may also be enough if you automate websites other than social media. If you automate social media, then with time you will need to advance your skills by learning how to build your own selectors in order to build truly robust TaskBots.

### Selectors: Introduction and Overview

{% embed url="<https://youtu.be/xRiai6UL7fQ?si=UJb-nB4Fjs_zhv_c>" %}


# Use Element's Text

If the element you want to address has unique text, you can address that element by that text. For example, if you want to automatically like a page on Facebook, you can make your TaskBot find and click on a button with text "Like".

Below you will see how exactly this can be done.

### **Exact Match**

#### ***text="Enter exact text of the element" (inside the double quotes)***

If you enter text inside double quotes, as shown below, your TaskBot will search for an element that matches exactly this text.&#x20;

<mark style="background-color:yellow;">text="some text"</mark>\
\
Beware: This option is case-sensitive!

For example, assume you want to start a post on LinkedIn, and the button on LinkedIn is called "Start a post".

![](/files/fbViz9k95IFTrbUZnZDO)

In this case, you can create a selector using the following syntax:&#x20;

<figure><img src="/files/wQ78IeNqyD3dFtJgaMfe" alt=""><figcaption></figcaption></figure>

### **Loose Match**

#### ***text=Enter SOME keywords of the text of the element*****&#x20;(without double quotes)**

If you enter text without double quotes, as shown below, your TaskBot will search for an element that has text that contains the keywords you provided.&#x20;

<mark style="background-color:yellow;">text=some keywords that the text contains</mark>

For example, assume you want to save the number of followers from a Facebook business page, and the information on the page contains a text that says *1,456 people follow this*.&#x20;

![](/files/OrrnTUh6i7L1hfQvuUjI)

\
As the number changes dynamically depending on the popularity of the page, you cannot use text="1,456 people follow this" (with double quotes) but you can use the keyword *people follow this*. Simply skip double quotes like shown here:&#x20;

<mark style="background-color:yellow;">text=people follow this (❌ instead of text="people follow this")</mark>

<figure><img src="/files/YYNSrGQFrYMOPYBcERLO" alt=""><figcaption></figcaption></figure>

### Video Tutorial: Introduction to Selectors

{% embed url="<https://youtu.be/xRiai6UL7fQ?si=UJb-nB4Fjs_zhv_c>" %}

### Video Tutorial: Example of Using text="" and text=

{% embed url="<https://youtu.be/NMBCdOIj4UU>" %}


# Copy Selectors

Copying selectors can be done very easily. Although directly copied selectors won't work for all use cases, they will work in **most** cases, and given how easy this method is, it's a great starting point.

This section explains how to copy selectors in one click.

We recommend this free CSS selector tool for Chrome: [Copy CSS Chrome plugin](https://chrome.google.com/webstore/detail/copy-css-selector/kemkenbgbgodoglfkkejbdcpojnodnkg). With it, you can right-click on any element of the website to copy its CSS selector and then paste it into the building block *Website action*.

![](/files/uuk4QNaYfjBRrFlRDzLD)

After clicking on *copy CSS selector*, wait a few seconds until a dotted frame is shown. If no frame is shown, then something went wrong - try right-clicking again until the frame appears. This is what this frame looks like:

![Wait for the frame to appear on the selected element](/files/pjlGR7mxkj1q8Rjcmowj)

### Video Tutorial: Introduction to Selectors

{% embed url="<https://youtu.be/xRiai6UL7fQ?si=UJb-nB4Fjs_zhv_c>" %}


# How to Build Custom Selectors

If copying selector does not produce reliable results, you need to build your own selector. We strongly recommend you to learn how to build your own selectors. Building custom selectors will allow you to create truly robust TaskBots that work reliably over time. Maybe you have tried other RPA or bot builder tools and are frustrated why bots break or do not work reliably over time. The answer is almost always this: You need more robust selectors, simply copying them is often not enough. And it's even more true if you build automations for social media.&#x20;

The great news is that you **do not need any coding skills** to learn how to do it!&#x20;

Here is what you need to learn in order to build your own selectors. Scroll below to find the video crash course as well.

**Note**: As mentioned earlier, ZeroWork supports both CSS and XPath selectors, but in this guide we will focus only on CSS selectors. More details on XPath: [How to Use XPath in ZeroWork (advanced)](/using-zerowork/using-selectors/how-to-use-xpath-in-zerowork-advanced).

{% content-ref url="/pages/r45r9ZgRfD5Y4ouScZtL" %}
[Basic Concepts of Selectors](/using-zerowork/using-selectors/how-to-build-custom-selectors/basic-concepts-of-selectors)
{% endcontent-ref %}

{% content-ref url="/pages/g8BCqzXrAB4jIcjq6yYJ" %}
[One Element Can Have Many Selector Expressions](/using-zerowork/using-selectors/how-to-build-custom-selectors/one-element-can-have-many-selector-expressions)
{% endcontent-ref %}

{% content-ref url="/pages/EKDG69w9MaaOt2RmZmVf" %}
[Check if Selector Is Correct and Unique](/using-zerowork/using-selectors/how-to-build-custom-selectors/check-if-selector-is-correct-and-unique)
{% endcontent-ref %}

{% content-ref url="/pages/b25igL4a0x2d3SuX7Bmc" %}
[Exact or Loose Match](/using-zerowork/using-selectors/how-to-build-custom-selectors/exact-or-loose-match)
{% endcontent-ref %}

{% content-ref url="/pages/sZ659tUB2Mv5DXB4GroP" %}
[Hierarchy](/using-zerowork/using-selectors/how-to-build-custom-selectors/hierarchy)
{% endcontent-ref %}

{% content-ref url="/pages/D3BbkTscxEK0urqT4CiV" %}
[Combine Filters with Standard CSS Logic](/using-zerowork/using-selectors/how-to-build-custom-selectors/combine-filters-with-standard-css-logic)
{% endcontent-ref %}

{% content-ref url="/pages/RaYYEU4A1n2vwZuFjllO" %}
[Lists: Incremental CSS Selectors](/using-zerowork/using-selectors/how-to-build-custom-selectors/lists-incremental-css-selectors)
{% endcontent-ref %}

{% content-ref url="/pages/JKf5KYiQShwVu8fMCDXk" %}
[Addressing Siblings](/using-zerowork/using-selectors/how-to-build-custom-selectors/addressing-siblings)
{% endcontent-ref %}

### Selector Crash Course: Complete Guide

{% embed url="<https://www.youtube.com/playlist?list=PL7X7ThrPNPJCVC6Df3M_d7tqeRQWPmF2r>" %}


# Basic Concepts of Selectors

Let's take the search button from the Wikipedia website as an example.&#x20;

For the record, for a website like Wikipedia it's fully sufficient to simply copy selectors, as described here: [Copy Selectors](/using-zerowork/using-selectors/copy-selectors). Building custom selectors is only needed if you create TaskBots for "tricky" websites such as social media. We are just using Wikipedia as an example to explain the basic concepts and the way of building your own CSS selectors.

Open your console by right-clicking on the search icon as shown below and then click on *Inspect*.

![](/files/aoO7CewTWQl0XWvDYnVX)

Now let's zoom in on the highlighted element on the right:

![](/files/rWxVX1P7BqX1r4B6qtT1)

#### **Tag**

Every CSS selector has a tag. Examples can be *div*, *a*, *main*, *li*, etc. It's the first word that is marked purple (see picture above).&#x20;

For Wikipedia's search button, the tag is ***button***.&#x20;

![](/files/DpoLfKhbyQoQRGKWEcOQ)

#### **Attributes**

Most CSS selectors have several attributes such as *class*, *id*, *aria-label*, etc. One selector can have one, many or no attributes. Attributes are marked brown (see picture above).

For Wikipedia's search button, the attributes are ***class*** and ***type.***&#x20;

![](/files/b50vs9aSOTrLAzgSlFJG)

#### **Value**

Inside the attributes there is a value.

For Wikipedia's search button, the value inside of ***class*** is *pure-button pure-button-primary-progressive* and the value inside of ***type*** is *submit*.

![](/files/zKZ0uuqBP1BmLZujix1a)

### **Building a Selector**

To build a selector, you need to adhere to this structure:

<mark style="background-color:yellow;">tag\[attribute='value']</mark>

Note that there is no space between tag and attribute in square brackets. Note that the value is wrapped in quotes.

Or, if your element has several attributes, then the structure can be:

<mark style="background-color:yellow;">tag\[attribute1='value']\[attribute2='value']</mark>

So for Wikipedia's search button in the above example, the selector would look like this:

<mark style="background-color:yellow;">button\[class='pure-button pure-button-primary-progressive']\[type='submit']</mark>

### **Video Tutorial: Basic Concepts**

{% embed url="<https://youtu.be/-W-ug4g_wjY>" %}


# One Element Can Have Many Selector Expressions

A selector can be built in many different ways. You can include all attributes. You can include only one attribute or some of the attributes. You can include attributes but no value (in the rare cases where the attributes are so unique in themselves). You can add a hierarchy (you will learn what hierarchy is in one of the next sections [Hierarchy](/using-zerowork/using-selectors/how-to-build-custom-selectors/hierarchy)). For example, the search button on Wikipedia can be expressed in many ways, and it will be addressed by any of the below:

<mark style="background-color:yellow;">button\[class='pure-button pure-button-primary-progressive']\[type='submit']</mark>

<mark style="background-color:yellow;">button\[type='submit']</mark>

<mark style="background-color:yellow;">button\[class='pure-button pure-button-primary-progressive']</mark>

<mark style="background-color:yellow;">button\[class\*='button']\[type='submit']</mark>

<mark style="background-color:yellow;">button\[class\*='button']</mark>

<mark style="background-color:yellow;">button\[class]\[type]</mark>

Will these selectors really work and address the same button? Yes!&#x20;

{% hint style="success" %}

#### One element - many ways to express its corresponding CSS selector!

{% endhint %}

So which should you choose? To get an answer to this question, read on 👇

### Adhere to Best Practices

* Try to create lean, short selectors. The more attributes your selectors contain, the more prone they are to fail in case something changes on the website.
* Try to not use any 'cryptic' text within the value of the attributes such as random letter combinations. Instead, try to identify unique values inside the attributes' values. A unique value is often descriptive of what the element is. If there are no descriptive values and unique attributes at all, then it might be better to only use hierarchy (more about hierarchy here: [Hierarchy](/using-zerowork/using-selectors/how-to-build-custom-selectors/hierarchy)) and discard all attributes and values.
* The more strict your hierarchy is (using many times ">" within your selector), the more prone your selector is to fail in case some element is removed from that hierarchy. If you are not sure what hierarchy is, continue reading this guide and come back to review this page at the end.

#### Examples

Based on these rules, let's examine reliability of the above list of selectors. To be clear, all of the above selectors are valid and your TaskBot will run successfully with any of them. In this section, we are only evaluating how robust these selectors are **over time**.&#x20;

❌ <mark style="background-color:yellow;">button\[class='pure-button pure-button-primary-progressive']\[type='submit']</mark>

**Why not**:

* Other selectors are shorter. Lean, short selectors tend to be more robust
* The value *pure-button pure-button-primary-progressive* appears more 'cryptic' and less descriptive compared to *submit* or *button.*

❌  <mark style="background-color:yellow;">button\[class='pure-button pure-button-primary-progressive']</mark>

**Why not**:

* The value *pure-button pure-button-primary-progressive* appears more 'cryptic' and less descriptive compared to *submit* or *button.*

❌ <mark style="background-color:yellow;">button\[class\*='button']</mark>

**Why not**:

* This might be too generic because there can be many buttons with value *button* in their class attribute on a page, so we should add an additional attribute or use a different attribute altogether to make this selector more unique, if possible.

❌ <mark style="background-color:yellow;">button\[class]\[type]</mark>

**Why not**:

* This might be too generic, too, because attributes *type* and *class* are very common, so we should add values to these attributes or add additional attributes.

✅ <mark style="background-color:yellow;">button\[class\*='button']\[type='submit']</mark>

**Why yes**:

* This selector uses two attributes with two concise, descriptive and well-identifiable values.

✅ <mark style="background-color:yellow;">button\[type='submit']</mark>

**Why yes**:

* While this selector might be a bit too generic and has some risk of not being unique, it's not common for a website to have multiple submit buttons on the same page, so this selector is a good choice, too.


# Check if Selector Is Correct and Unique

Here is how you can check whether a selector is correct and unique before you run your TaskBot.

Click on the *Console* tab inside your *Inspect* mode.&#x20;

![](/files/SC09mYrwCZ3P9jkqoWoJ)

Now enter the following command directly on the console:

<mark style="background-color:yellow;">document.querySelectorAll("PASTE YOUR SELECTOR HERE")</mark>

In the example of Wikipedia's search button, it can be, for example, this selector (but you can use any other from the list you saw in the previous chapter [One Element Can Have Many Selector Expressions](/using-zerowork/using-selectors/how-to-build-custom-selectors/one-element-can-have-many-selector-expressions)):

<mark style="background-color:yellow;">document.querySelectorAll("button\[class\*='button']\[type='submit']")</mark>

This is what it looks like in action:

![](/files/CDIOpQIoYXS8b5gNYKj0)

Once you hit *Enter*, you will see that there is an element shown:

![](/files/LuhQ9TxpK4eMytkvlu0I)

Once you expand the shown element and hover over it, you will see the button highlighted on the left side that is identified by that selector.

![](/files/UrqlKs0qEtqEjOJj7VUF)

If your selector is not unique enough, the result will show multiple selectors. This means you need to get back to work and build a more unique selector. **Exception**: Unless you are building a selector for a list (such as list of profiles, list of pages, etc.) - in this case keep reading this guide and be sure to pay attention in the section[Lists: Incremental CSS Selectors](/using-zerowork/using-selectors/how-to-build-custom-selectors/lists-incremental-css-selectors).

If the result is not showing anything, this means your selector is not working out or you have a typo inside it (for example, you used double quotes instead of single quotes). You need to try again, adjust it or build another one.

When you build your own selectors, always first check them via *document.querySelectorAll* directly on the website. This way you can avoid running your TaskBot every time you want to check validity of your custom built selector. It will save you a lot of time.

{% hint style="warning" %}
Note that you cannot check correctness of text= (see [Use Element's Text](/using-zerowork/using-selectors/use-elements-text)) with *document.querySelectorAll* because this method is unique to ZeroWork.
{% endhint %}

### **Video Tutorial: Use document.querySelectorAll to Check Selector's Correctness and Uniqueness**

{% embed url="<https://youtu.be/hHAEaaxHEUI>" %}


# Exact or Loose Match

Instead of = to define the value of the attribute, you can use \*=. The sign = means exact match and \*= means loose match.

In the example of the Wikipedia's search button ([Basic Concepts of Selectors](/using-zerowork/using-selectors/how-to-build-custom-selectors/basic-concepts-of-selectors)), we created a selector that has an exact match. To remind you, this was the selector we've built:

<mark style="background-color:yellow;">button\[class='pure-button pure-button-primary-progressive']\[type='submit']</mark>

But if you use a loose match, the selector can look like this:

<mark style="background-color:yellow;">button\[class\*='pure-button']\[type='submit']</mark>

Note how exact value of *pure-button pure-button-primary-progressive* was shortened to the loosely matching value *pure-button*.&#x20;

### **Video Tutorial: Exact versus Loose Match**

{% embed url="<https://youtu.be/tQvTy4Rk5wc>" %}

### Example: Exact versus Loose Match

{% embed url="<https://youtu.be/kOIzwTqeKnY>" %}


# Hierarchy

Understanding hierarchy is one of the most important steps when it comes to building custom selectors. Do not skip this section!

Every web element on a website adheres to a hierarchy. See the hierarchy for the search button on the Wikipedia page:

![](/files/Gme241YQ1PtV8FP69jqU)

For the search button on Wikipedia in the above example, the hierarchy would be:&#x20;

<mark style="background-color:yellow;">body > div > form > fieldset > button</mark>

Note that some elements are on the same level (see picture below). This means these elements are items in a list and siblings to each other. They cannot be defined in a hierarchical way as shown in the example above (with >). They can be either defined as a list (you will learn about it later in chapter [Lists: Incremental CSS Selectors](/using-zerowork/using-selectors/how-to-build-custom-selectors/lists-incremental-css-selectors)) or - which is a bit more advanced - as siblings (you will learn about it in the final chapter of this guide [Addressing Siblings](/using-zerowork/using-selectors/how-to-build-custom-selectors/addressing-siblings)).

![](/files/FaE09DkZt6CfeUiCwlrM)

### Strict and Loose Hierarchy

#### Strict hierarchy

Whenever you use >, the hierarchy is defined in a strict way - in the example above (*body > div > form > fieldset > button*), *body* MUST be followed by *div* and if not the selector will fail.&#x20;

#### Loose hierarchy

Alternatively, you can use space to define a loose hierarchy, for example:&#x20;

<mark style="background-color:yellow;">body button</mark>

This means that *body* is followed by *button* somewhere below and it doesn't matter if there are other elements between *body* and *button*.

### Build Selectors Based on Hierarchy

In some cases, to build a selector, you may need to additionally define the hierarchy. You can simply define tags that follow each other, like this:

**Strict hierarchy:** <mark style="background-color:yellow;">tag1 > tag2 > tag3 > tag4 > tag5</mark>

**Loose hierarchy:** <mark style="background-color:yellow;">tag1 tag3 tag5</mark> (did you note how tag2 and tag4 are skipped?)&#x20;

**Combination of strict and loose hierarchy:** <mark style="background-color:yellow;">tag1 tag3 > tag4</mark> (note how tag2 is skipped but tag4 strictly follows tag3)

And for any tag inside the hierarchy you can add attributes like this:

<mark style="background-color:yellow;">tag1\[attribute='value'] > tag2\[attribute='value']</mark>

<mark style="background-color:yellow;">tag1 > tag2\[attribute='value']</mark>

<mark style="background-color:yellow;">tag1\[attribute='value'] > tag2</mark>

For the example of the search button on Wikipedia, the list of selectors we built can be extended by these ones, and they will work just as well:

**Strict hierarchy:** <mark style="background-color:yellow;">body > div > form > fieldset > button\[class\*='pure-button']</mark>

**Loose hierarchy:** <mark style="background-color:yellow;">body form button\[class\*='pure-button']</mark>

**Combination of strict and loose hierarchy:** <mark style="background-color:yellow;">body > div button\[class\*='pure-button']</mark>

### Video Tutorial: Selector Hierarchy

{% embed url="<https://youtu.be/Z8-JNTNRLrA>" %}


# Combine Filters with Standard CSS Logic

You can combine ZeroWork's *text=* method with the standard selector logic by using >> as a filter.

As an imaginary example, you can define a selector like this:

<mark style="background-color:yellow;">div > div\[role='main'] > div div >> text="Like"</mark>

{% hint style="warning" %}
\>> can only be added **at the end of the selector**. \
\
✅ This will work: <mark style="background-color:yellow;">div > div\[role='main'] > div div >> text="Like"</mark>\
❌ This will not work: <mark style="background-color:yellow;">div > div\[role='main'] > div >> text="Like" > div</mark>
{% endhint %}

### Video Tutorial: Combine text= with Standard Selector Logic

{% embed url="<https://youtu.be/mQBMNaEoE_o>" %}

### Example: Save Email Addresses from Facebook Pages by Combining text= with Standard Selector Logic

{% embed url="<https://youtu.be/7J5udyEt7ZY>" %}


# Lists: Incremental CSS Selectors

This is another very crucial section that you need to understand in order to be able to build selectors. So do not skip this one!

Selectors have incremental numbers in lists.&#x20;

For example, imagine that you want to store a complete list of hundreds of contacts from LinkedIn. In this case, the web elements (e.g., profile name, profile link, job position, etc.) inside of that list have an **incremental** selector.&#x20;

### When Copying Selectors

Let's just copy the selectors of the profiles on LinkedIn’s search results. By copy, we mean this way of getting selectors: [Copy Selectors](/using-zerowork/using-selectors/copy-selectors). In this case, the selector corresponds to the contact’s profile name (first and last name):

li:nth-child(<mark style="color:red;">**1**</mark>) > div.abi-saved-contacts-row\_\_details > button\[type=”button”] > div > div > span

li:nth-child(<mark style="color:red;">**2**</mark>) > div.abi-saved-contacts-row\_\_details > button\[type=”button”] > div > div > span

As you can see, each CSS selector has a number that increments. For your convenience, we highlighted the incrementing number in red (see above).\
\
Now imagine that you want to save the entire list of these 100 contacts. To accomplish this, refer to the example in [Save Lists](/using-zerowork/using-building-blocks/save-web-element/save-lists) and [Standard Loop](/using-zerowork/using-building-blocks/start-repeat/standard-loop#how-to-use-loop-index-syntax-to-save-lists-of-public-data).

### When Building Custom Selectors

Let's continue with the example where you want to save a list of profiles from LinkedIn. As you have already learned, all the selectors in such a list will be the same and have an incremental number (as described above).

Now, however, you want to build your own custom selector.

There are two ways of dealing with it.

#### Method one: Use *>> nth=<mark style="color:red;">number</mark>*

Use an addition in the form of ***>> nth=0*** where 0 represents the incremental number, so it can be changed to *nth=1, nth=2*, etc., depending on which element in the list you want to address. Note that you cannot check correctness of it via *document.querySelectorAll* because this method is unique to ZeroWork.

Let's start with building the selector for name and surname on LinkedIn Sales Navigator (you can use LinkedIn free version instead -> you will just define a different selector compared to what you see below, but the steps are the same). You can see tag and attributes in the picture below.

![](/files/somzSBSq0wCnluuAeEYc)

A selector that proved robust enough is:&#x20;

<mark style="background-color:yellow;">a\[data-anonymize='person-name']</mark>

By entering it on the console and executing *document.querySelectorAll("a\[data-anonymize='person-name']")*, you will get 25 results which correspond to 25 profiles on the page. (You will get 10 on LinkedIn free version.) This is exactly what we are looking for - remember the selectors for the items of the list are the **same**, and this is why the result is not unique.

You can now use *>> nth=* which would look like this:

<mark style="background-color:yellow;">a\[data-anonymize='person-name'] >> nth=0</mark>

0 corresponds to the first item in the list. (*>>nth=* is a zero based system, meaning that 0 corresponds to the first item, 1 corresponds to the second item, etc.).&#x20;

If you change 0 to 1, then you would be addressing the second item in this list:&#x20;

<mark style="background-color:yellow;">a\[data-anonymize='person-name'] >> nth=1</mark>

If you want to address the whole list, you need to use {loop\_index,1}, which is specific to ZeroWork application, and you can learn more about it here: [Standard Loop](/using-zerowork/using-building-blocks/start-repeat/standard-loop#how-save-a-list-not-just-one-profile).

#### Method two: Use *:nth-child(<mark style="color:red;">number</mark>)*

Another way is to add an addition to the incrementing element in the form of ***:nth-child(1)***, where 1 represents the incremental number, so likewise it can be changed to *:nth-child(2), :nth-child(3),* etc., depending on which element in the list you want to address.&#x20;

The difference from the first method is that you need to find out which tag in the hierarchy structure increments and thus represents the list. This makes this method more difficult but also more precise.

Let's continue with the same example where the selector was this:

<mark style="background-color:yellow;">a\[data-anonymize='person-name']</mark>

Now, your next step is to analyze the elements in the hierarchy to find out which one represent an incremental list. You may need to expand and collapse different elements to get an overview. At some point, you will see the structure shown below. You can see the incremental element with *li* as a tag, which corresponds to every card in the list. You can recognize it as list because all these *li*-tags are listed on the same level and when you hover over them, the elements in your list are highlighted.

![](/files/DYC7qBKiJeoMmuKOlXNK)

Since the selector for name and surname is inside that element, you would need to place it after *li* to reflect the hierarchy. The final selector for this list would be:&#x20;

<mark style="background-color:yellow;">li:nth-child(1) a\[data-anonymize='person-name']</mark>

1 corresponds to the first item in the list. Note that :nth-child() is **not** a zero based system, meaning that 1 corresponds to the first item, 2 corresponds to the second item, etc.&#x20;

If you change 1 to 2, then you would be addressing the second item in this list:&#x20;

<mark style="background-color:yellow;">li:nth-child(2) a\[data-anonymize='person-name']</mark>

The way you can address the whole list is by using {loop\_index,1} again, as explained here: [Standard Loop](/using-zerowork/using-building-blocks/start-repeat/standard-loop#how-save-a-list-not-just-one-profile).

### Video Tutorial: Define Selectors for Lists

{% embed url="<https://youtu.be/xTplANedrAk>" %}

### Example: Identify a Selector for a List of Questions on Quora

{% embed url="<https://youtu.be/sm6mOtEGyd8>" %}

### Example: Save a List of Pages from Facebook

{% embed url="<https://youtu.be/1TCl8kE3KdE>" %}


# Addressing Siblings

This is a more advanced concept and it's typically not needed frequently. So feel free to skip this section.

As described in section [Hierarchy](/using-zerowork/using-selectors/how-to-build-custom-selectors/hierarchy), elements can be defined by their hierarchy if they follow each other in a 'narrowing down' fashion. That means they can't be on the same level. If you need to address a tag which is on the same level but cannot or is not intended to be addressed in a list (list as described here [Lists: Incremental CSS Selectors](/using-zerowork/using-selectors/how-to-build-custom-selectors/lists-incremental-css-selectors)), you can use tilde \~ for loose sibling (any of the following elements on that hierarchy level) and + for exact sibling (the next element on that hierarchy level). \
\
This might be confusing at a first glance, so you might want to check the video below for a more hands-on demonstration.

### Video Tutorial: Siblings

{% embed url="<https://youtu.be/5Wg0WD4MQLU>" %}


# How to Use XPath in ZeroWork (advanced)

As mentioned previously, ZeroWork supports both CSS and XPath selectors. In this documentation, we are mostly focusing on CSS selectors only, as they are more beginner- and intermediate-friendly. We expect more advanced users to be already familiar with how to use XPath.

TaskBots will automatically recognize an XPath selector, when it starts with double slash //. Example below.&#x20;

<figure><img src="/files/jFbsLEWP2Po3olSxWNH8" alt=""><figcaption></figcaption></figure>

Additionally, you can also explicitly state xpath like so: `xpath=//button`.

To iterate over a list of incrementing XPath selectors, use the same syntax with loop\_index, as explained here: [Standard Loop](/using-zerowork/using-building-blocks/start-repeat/standard-loop#how-to-use-loop-index-syntax-to-save-lists-of-public-data). Example below.

<figure><img src="/files/uLQ8DycW29JrXCK8Dp0b" alt=""><figcaption></figcaption></figure>


# CSS Selectors Master Class

{% embed url="<https://www.youtube.com/watch?v=uhzUHlxwHQw>" %}


# Using Building Blocks

## 1. Dynamic Inputs

All building block input fields support:

* References to variables and tables — "Hello {id: 123, name: "Profile name"}
* Code in inputs — "This is my secret password stored only locally: ${await zw\.deviceStorage.get("password")}"
* Spintax to randomize input — "{ Hi | Hey | Howdy }! How { are you | are things going }?" 

{% content-ref url="/pages/VkBlIOlZTIbg4RP3xRKC" %}
[Dynamic Inputs](/using-zerowork/using-building-blocks/dynamic-inputs)
{% endcontent-ref %}

***

## 2. Building Blocks

Below are the documentation pages for all building blocks in ZeroWork.

### Browser

{% content-ref url="/pages/395xCRwk7hIpu8ucOxeJ" %}
[Open Link](/using-zerowork/using-building-blocks/open-link)
{% endcontent-ref %}

{% content-ref url="/pages/MyN4fbozS2oQBkwzciwz" %}
[Save Page URL](/using-zerowork/using-building-blocks/save-page-url)
{% endcontent-ref %}

{% content-ref url="/pages/KBC6dSNlH0iyXVZeMhDB" %}
[Switch or Close Tab](/using-zerowork/using-building-blocks/switch-or-close-tab)
{% endcontent-ref %}

{% content-ref url="/pages/9rFFZv94xKnuoUfhE2US" %}
[Go Back or Forward](/using-zerowork/using-building-blocks/go-back-or-forward)
{% endcontent-ref %}

{% content-ref url="/pages/bruz6DvHGXIE42Ky8Bu5" %}
[Switch Frame](/using-zerowork/using-building-blocks/switch-frame)
{% endcontent-ref %}

{% content-ref url="/pages/jkYBGt5bEJHYayoHDaub" %}
[Browser Alert](/using-zerowork/using-building-blocks/browser-alert)
{% endcontent-ref %}

### Web Interaction

{% content-ref url="/pages/vTKkFCwMGX2mIbXQ0iy4" %}
[Click Web Element](/using-zerowork/using-building-blocks/click-web-element)
{% endcontent-ref %}

{% content-ref url="/pages/kQceSds9tcs3jxj5IAqK" %}
[Check Web Element](/using-zerowork/using-building-blocks/check-web-element)
{% endcontent-ref %}

{% content-ref url="/pages/3BBsordNOUt53T7iLulN" %}
[Save Web Element](/using-zerowork/using-building-blocks/save-web-element)
{% endcontent-ref %}

{% content-ref url="/pages/soXP153aHKrdSwiEI2Gv" %}
[Insert Text or Data](/using-zerowork/using-building-blocks/insert-text-or-data)
{% endcontent-ref %}

{% content-ref url="/pages/7fuQCdYr3LTFDQSNbdQt" %}
[Hover Web Element](/using-zerowork/using-building-blocks/hover-web-element)
{% endcontent-ref %}

{% content-ref url="/pages/HvgEbrulNTVpOhTDHwT5" %}
[Select Web Dropdown](/using-zerowork/using-building-blocks/select-web-dropdown)
{% endcontent-ref %}

{% content-ref url="/pages/3z2ExPrfLntkc8y5NjNg" %}
[Keyboard Action](/using-zerowork/using-building-blocks/keyboard-action)
{% endcontent-ref %}

### Logic

{% content-ref url="/pages/xR120TGdP5r7VVuRMvOK" %}
[Start Condition and Set Condition](/using-zerowork/using-building-blocks/start-condition-and-set-condition)
{% endcontent-ref %}

{% content-ref url="/pages/NiZ1cE6yPtP9FuLysNpX" %}
[Start Repeat](/using-zerowork/using-building-blocks/start-repeat)
{% endcontent-ref %}

{% content-ref url="/pages/tCjjoxBw5MNrmzRvjLNe" %}
[After Repeat](/using-zerowork/using-building-blocks/after-repeat)
{% endcontent-ref %}

{% content-ref url="/pages/dD45R9cnUfZNqLZn3OOD" %}
[Break Repeat](/using-zerowork/using-building-blocks/break-repeat)
{% endcontent-ref %}

{% content-ref url="/pages/24PZyzWJjXsZra1cGqlE" %}
[Try-Catch](/using-zerowork/using-building-blocks/try-catch)
{% endcontent-ref %}

{% content-ref url="/pages/Fg4z6uynTlHywPJH7JQ7" %}
[Raise Error](/using-zerowork/using-building-blocks/raise-error)
{% endcontent-ref %}

{% content-ref url="/pages/3WmV2GBGxOPtF4E51326" %}
[Abort Run](/using-zerowork/using-building-blocks/abort-run)
{% endcontent-ref %}

### Data

{% content-ref url="/pages/WjtHtxxX09r0BXJwbGPQ" %}
[Update Data](/using-zerowork/using-building-blocks/update-data)
{% endcontent-ref %}

{% content-ref url="/pages/gGjUGgcUZiwF5qZY5GE7" %}
[Number Operations](/using-zerowork/using-building-blocks/number-operations)
{% endcontent-ref %}

{% content-ref url="/pages/1mWu5qFblHQ2gh7aaQNJ" %}
[Format Data](/using-zerowork/using-building-blocks/format-data)
{% endcontent-ref %}

{% content-ref url="/pages/CMCgN6FKFGx73MwE8FbK" %}
[Split Data](/using-zerowork/using-building-blocks/split-data)
{% endcontent-ref %}

{% content-ref url="/pages/q454BCStXMugHN4n137O" %}
[Apply Regex](/using-zerowork/using-building-blocks/apply-regex)
{% endcontent-ref %}

{% content-ref url="/pages/fWqv5wn2OPeHK8E5rOEW" %}
[Remove Duplicates](/using-zerowork/using-building-blocks/remove-duplicates)
{% endcontent-ref %}

{% content-ref url="/pages/mh5KZXcNV4OwT7uOet3A" %}
[Delete Data](/using-zerowork/using-building-blocks/delete-data)
{% endcontent-ref %}

### External

{% content-ref url="/pages/Dl2IiCB18XngokBNQ0h4" %}
[Ask ChatGPT](/using-zerowork/using-building-blocks/ask-chatgpt)
{% endcontent-ref %}

{% content-ref url="/pages/MwEiUpd8bJY336QUCIrH" %}
[Send Notification](/using-zerowork/using-building-blocks/send-notification)
{% endcontent-ref %}

{% content-ref url="/pages/f1o1B7ewaH1ptL5WMkC3" %}
[APIs: Send HTTP Request](/using-zerowork/using-building-blocks/apis-send-http-request)
{% endcontent-ref %}

{% content-ref url="/pages/MF29c3EkhotatZrl92B3" %}
[Write JavaScript](/using-zerowork/using-building-blocks/write-javascript)
{% endcontent-ref %}

### Files

{% content-ref url="/pages/1vfTS9H0fY0WetiPdCTL" %}
[Save File](/using-zerowork/using-building-blocks/save-file)
{% endcontent-ref %}

{% content-ref url="/pages/Jqa9Q68c9yuP5nkL0ISP" %}
[Upload File](/using-zerowork/using-building-blocks/upload-file)
{% endcontent-ref %}

### Tools

{% content-ref url="/pages/lQgCqnKaRT3Xk0vJ1lUc" %}
[Delay](/using-zerowork/using-building-blocks/delay)
{% endcontent-ref %}

{% content-ref url="/pages/nYcOryk5Vrroy6T1h7RG" %}
[Record Date](/using-zerowork/using-building-blocks/record-date)
{% endcontent-ref %}

{% content-ref url="/pages/0EjYWrZ1pfCijb8rJweW" %}
[Take Screenshot](/using-zerowork/using-building-blocks/take-screenshot)
{% endcontent-ref %}

{% content-ref url="/pages/AD0SWa5wZy0d5NpYLgjN" %}
[Save from Clipboard](/using-zerowork/using-building-blocks/save-from-clipboard)
{% endcontent-ref %}

{% content-ref url="/pages/Av5BGBAs3WiVGjOU4nLb" %}
[Log](/using-zerowork/using-building-blocks/log)
{% endcontent-ref %}

***

## 3. Building Block Options

Learn about options like deactivating building blocks, skipping a loop, auto-aligning blocks, and selecting multiple blocks.

{% content-ref url="/pages/PIfcQHmWHQmsg7qUUBmK" %}
[Building Block Options](/using-zerowork/using-building-blocks/building-block-options)
{% endcontent-ref %}


# Dynamic Inputs

All building block input fields accept dynamic inputs.

#### **References to Variables and Tables**

**Example**

* "Hello {id: 123, name: "Profile name"}, I wanted to reach out because...."

{% content-ref url="/pages/8Z5QCnjLyMAUf8q7w9Tb" %}
[References to Variables and Tables](/using-zerowork/using-building-blocks/dynamic-inputs/references-to-variables-and-tables)
{% endcontent-ref %}

#### Code in Inputs

***Code in inputs*** ***is available when using Desktop Agent version 1.1.72 and higher***

Use `${...}` for an expression and `$${...}` for a code block. Expressions insert the result of the expression. Code blocks run full code and must `return` a value to insert.

**Examples**

* **Using `${...}` (expression)**\
  "This is my secret password stored locally: ${await zw\.deviceStorage.get("password")}"
* **Using `$${...}` (code block)**\
  "This is my more elaborate input: $${\
  &#x20;  import axios from "axios"; // any import auto-installs locally\
  &#x20;  const res = await axios.get("<https://mydomain.com/my/api/url");\\>
  &#x20;  return res.data;\
  }."

{% content-ref url="/pages/cQADB6rzTXDMYBnlrrSR" %}
[Code in Inputs](/using-zerowork/using-building-blocks/dynamic-inputs/code-in-inputs)
{% endcontent-ref %}

#### **Spintax**

Helps vary content on sites with anti-spam measures that check message or email uniqueness.

**Example**

* "{ Hi | Hey | Howdy }! How { are you | are things going }?"

{% content-ref url="/pages/GbCANNIXV812fhJ3AxYc" %}
[Spintax](/using-zerowork/using-building-blocks/dynamic-inputs/spintax)
{% endcontent-ref %}


# References to Variables and Tables

Click on the **V** (for [variables](/using-zerowork/using-variables)) or **T** (for [tables](/using-zerowork/using-tables)) at the top right of your building block to add a reference to a table or variable.

<figure><img src="/files/XrvaufqXMd5r5kotwJNo" alt=""><figcaption></figcaption></figure>


# Code in Inputs

Use `${...}` for an expression and `$${...}` for a code block. Expressions insert their result. Code blocks run full code and must `return` a value to insert. All code runs locally on your device.

***

#### **Using `${...}` (expression)**

**Examples**

* "This is my secret password stored locally: ${`await zw.deviceStorage.get("password")`}"
* "This is my random number: ${`Math.random()`}"
* "This is my formatted input ${`"{id: 1234, name: Country Code}".trim().toUpperCase()`} \
  ⚠️ **Note**: table/variable references like `{id: 1234, name: Country Code}` are **not** code; they’re replaced with their values. If you need that value **as a string** inside code, wrap it in quotes (as above) or read it with `zw.getRef`: ${`(await zw.getRef({ ref_id: 1234, name: "Country Code" })).trim().toUpperCase()`}".

***

#### **Using `$${...}` (code block)**

**Examples:**

* "This is a value returned from an API: $${

  ```
  import axios from "axios"; // any import auto-installs locally
  const res = await axios.get("https://mydomain.com/my/api/url");
  return res.data;
  ```

&#x20;       }."

* "This is my input with more complex formatting: $${

  ```
  const fullAddress = await zw.getRef(ref_id: 1234, name: "Address" });
  const splitValues = fullAddress.split("country code: ");
  const countryCode = splitValues[1];

  if (!countryCode) {
    await zw.log({ message: "no country code", status: "warning" });
    return "unknown";
  }

  return countryCode.toUpperCase().trim();
  ```

  }."

***

<figure><img src="/files/Fc37E0prMkxKxrHfhjr1" alt=""><figcaption></figcaption></figure>


# Spintax

Spintax lets you randomly vary text. This can be helpful on sites with anti-spam measures that check whether messages or emails are unique.

For example, you can enter text like this:

**{hi|hello|hey|howdy}**

The TaskBot will randomly pick one of the provided options.

<figure><img src="/files/pG7sTOYkP7LTqUM0g6Va" alt=""><figcaption></figcaption></figure>

#### In which building blocks can I use spintax?

You can use spintax in the **Update Data** and **Insert Text or Data** building blocks.

In **Update Data**, spintax is disabled by default. To enable it, check the **Use spintax** checkbox.


# Open Link

This action opens a link in a Chrome window.

#### Example input

<figure><img src="/files/VQeZ4EpubBu8eN191H1U" alt=""><figcaption></figcaption></figure>

#### Using a variable or table reference

<figure><img src="/files/IB7mJMEbBPZ6YImgDeg1" alt=""><figcaption></figcaption></figure>

### Open in New Tab

To open the website in a new tab, simply check the checkbox shown below:

<figure><img src="/files/Xnu71lxyEPN6S7aCla1G" alt=""><figcaption></figcaption></figure>

Note that when new tab is opened, the tab stays in the background. This is normal. The TaskBot makes a new tab active but it doesn't bring it to the front. This doesn't impede TaskBot run and doesn't impede it from operating in that new tab. If this is inconvenient for you because this impedes you from watching TaskBot perform its actions, either click manually on the tab that the TaskBot switched to or add *Switch or Close Tab* building block to close the tabs that are inactive.

### Website Network Errors

Whenever your TaskBot cannot reach a website url, it will stop its run and issue an error report that you can check in your TaskBot run reports (see [Using Run Reports](/using-zerowork/using-run-reports)). The issue is usually that the website is not available, is taking too long to load, your internet connection is too slow or your proxy settings are not valid.

In the error message, you will find the error code that your TaskBot received. Since there are over 200 network error codes, it would go beyond the scope of this documentation page to explain each. However, there are many publicly available resources online such as blog articles and Chrome help docs that provide information about each specific error. So if a particular error happens to you frequently, it could be helpful to google that specific error code and read about possible solutions. You might find that the error is happening due to your specific VPN, antivirus or router set-up.

You can find the complete list of network error codes here: chrome://network-errors/.

This is how the error code is displayed in your error report:

<figure><img src="/files/ItlNDswHUTdxLjBKcFFn" alt=""><figcaption></figcaption></figure>

### FAQ

#### How do I make TaskBot log in to my account when it opens a website?

Please see [Cookies](/using-zerowork/general-run-schedule-share-webhooks/cookies).

#### When I use a table reference, I get an error that there was no url.

Please see [When I Use Data from Table, No Data is Being Pulled](/using-zerowork/common-problems/when-i-use-data-from-table-no-data-is-being-pulled).

### Video Tutorial

{% embed url="<https://youtu.be/N8kRvfu6nB0?si=S1Z13osY2CXeRiUn>" %}


# Save Page URL

This action saves the url of the page that your TaskBot is currently visiting.

For example, you can create a variable where currently visited url is saved to.

<figure><img src="/files/CXmzijtPg25jZ8zpb7zD" alt=""><figcaption></figcaption></figure>


# Switch or Close Tab

The **Switch or Close Tab** building block allows you to:

* Switch to a specific tab using different criteria.
* Close a tab.

### **Options**

* **Latest tab**: Switch to the last tab on the farthest right.
* **Previous tab**: Switch to the tab directly to the left of the current tab.
* **Next tab**: Switch to the tab directly to the right of the current tab.
* **Tab URL matching**: Match a tab based on its URL or part of it. If there are multiple matching tabs, the first one will be picked. Examples:
  * A full URL (e.g., `google.com`).
  * A partial URL (e.g., `/subpage`).
  * For advanced matching: A regular expression entered between slashes with or without a flag (e.g., `/^https:\/\/google\.com$/i` or `/^https:\/\/google\.com$/`). If your regular expression is invalid, no error will be thrown and TaskBot will assume it is a non-regex value. To ensure that your expression is correct, use regex validator beforehand (e.g. <https://regex101.com/>).
* **Tab number**: Switch to a tab by its position in the browser. For example:
  * **Tab number 1**: First tab on the farthest left.
  * **Tab number 2**: Second tab from the left, and so on.

{% hint style="info" %}
Be cautious with options other than *Tab URL matching.* TaskBot determines the order of tabs based on their creation time. If you manually rearrange tabs during the TaskBot's run, the tab number may no longer match the visual order displayed in the browser.
{% endhint %}

{% hint style="warning" %}
When using the *Run in my regular browser* run mode, the order of the tabs cannot be guaranteed due to parallel TaskBot runs within the same context, as well as the presence of your own tabs. Prefer using the **Tab URL Matching** option instead.
{% endhint %}

### **Actions**

#### **1. Switch to Tab**

Switch to a tab based on the criteria described above.&#x20;

#### **2. Close Tab**

Close a specific tab based on the criteria described above.

**What happens if a currently active tab is closed?**

If you close the currently active tab:

* The next tab to the right will become active.
* If no tabs exist on the right, the next tab to the left becomes active.
* If no tabs remain, the browser application will close.

If the tab that is closed is *not* active, the active tab will remain as it is.

If this default behavior does not meet your needs, you can add another **Switch or Close Tab** building block to define which tab should become active.

If the browser application closes because all tabs are closed, TaskBot will continue running. However, if subsequent actions require a browser (such as a Click Web Element building block), TaskBot will stop, generate an error, and report the run as failed. To prevent this, you can add an Open Link building block to re-launch the browser.


# Go Back or Forward

The **Go Back or Forward** building block allows your TaskBot to navigate browser history by moving backward or forward.&#x20;

If there is no page to go back or forward to, the action will be non-consequential and TaskBot will continue to the next action.


# Launch Browser

<figure><img src="/files/ToSpVJ9KFYVgUquQhtzR" alt=""><figcaption></figcaption></figure>

The **Launch Browser** block allows you to launch a browser for your TaskBot to use. Subsequent no-code web-interaction blocks (like Click Web Element, Save Web Element, etc) act on the browser context this block launches.

{% hint style="success" %}
This block is one of several ways to launch a browser. You can also launch with an [Open Link](/using-zerowork/using-building-blocks/open-link) block or programmatically via `zw.browserContext.launch()` in a [Write JS](/using-zerowork/using-building-blocks/write-javascript) block. For the latter, see [Browser Context](/using-zerowork/using-building-blocks/write-javascript/browser-context) for the full API.
{% endhint %}

#### How To Use It

This block works with no configuration.

You might not need to use this block. Every setting listed below defaults to **Use current defaults**, which inherits from your TaskBot's **Browser Launch Settings** or any values previously set via `zw.browserContext.setDefaults()` in a Write JS block.

This block lets you override individual settings as needed *during* a TaskBot's run without a Write JS block.

{% hint style="success" %}
These overridden settings become the new defaults for this TaskBot at runtime. To reset or undo, add another Launch Browser block that reverses the previous Launch Browser block's settings prior to subsequent blocks.
{% endhint %}

***

**Launch mode**

<figure><img src="/files/E1wRxekN8WAAMaOVFTDg" alt=""><figcaption></figcaption></figure>

Controls how the browser session is managed.

* **Use current defaults** inherits from your TaskBot's Browser Launch Settings.
* When unchecked, you can choose between:
  * **Incognito** an isolated browser session. No data persists between runs.
  * **Sticky** a persistent browser profile. Cookies, storage, and login state survive between runs. Enter your **Sticky profile ID**. Clicking **COPY PROFILE ID** allows you to select from a list of your available profiles. Your selected sticky profile is copied to your clipboard; paste it into the **Sticky profile ID** field.

{% hint style="success" %}
You can create new sticky profiles in **Browser Launch Settings** to make them available here.
{% endhint %}

***

**Run mode & window**

<figure><img src="/files/aSPEcWkqUCUKerNvFeTX" alt=""><figcaption></figcaption></figure>

**Bypass bot detection**

When enabled, your TaskBot is no longer detectable as bot automation.

{% hint style="warning" %}
When Bypass bot detection is enabled, some settings are ignored (Window size, Launch arguments, Browser) and file uploads larger than approximately 50 MB are blocked. File downloads are unaffected.
{% endhint %}

**Run in background**

Runs the browser in the background with no visible window. Uses fewer resources.

{% hint style="success" %}
When running in background, the browser uses a fixed viewport. If you don't set a **Window size**, the default (1280 x 720) is used.
{% endhint %}

**Maximize**

Maximizes the browser window on launch. Ignored when running in background.

**Window size**

Sets the browser viewport dimensions. This setting only takes effect when **Maximize** is off or **Run in background** is on.

* **Use current defaults** inherits the current viewport size.
* When unchecked, you can set **Width** and **Height** in pixels.

***

**Cookies**

<figure><img src="/files/yM6YtyeZ9qL6ehM8gZBj" alt=""><figcaption></figcaption></figure>

Provide cookies up front so they are reapplied on every context relaunch.

* **Use current defaults** inherits cookies from your Browser Launch Settings.
* When unchecked, paste cookie JSON into the text field. Each cookie should include at least `name`, `value`, and `domain`.
* Click **+ ADD COOKIE** to add cookies from additional websites. Click the trash icon to remove one.

{% hint style="success" %}
See [Cookies](/using-zerowork/general-run-schedule-share-webhooks/cookies) for how to export cookies from your browser and use them in ZeroWork.
{% endhint %}

***

**Proxy**

<figure><img src="/files/3mFJ4UsNZYd3sTuYgddx" alt=""><figcaption></figcaption></figure>

Route browser traffic through a proxy server.

* **Use current defaults** inherits proxy settings from your Browser Launch Settings.
* When unchecked:
  * **Proxy server** `host:port` or `socks5://host:port` for SOCKS5 proxies.
  * **Username** HTTP proxy authentication. Not supported for SOCKS5.
  * **Password** HTTP proxy authentication. Not supported for SOCKS5.
  * **Bypass domains** comma-separated domains to bypass the proxy (for example, `example.com, google.com`)

{% hint style="success" %}
See [Proxies](/using-zerowork/general-run-schedule-share-webhooks/proxies) for more info on how to use proxies in ZeroWork.
{% endhint %}

***

**Browser**

<figure><img src="/files/hpjJIRKCIIHGUOrQrS3H" alt=""><figcaption></figcaption></figure>

Controls which browser executable to use at runtime.

* **Use current defaults** inherits from your Browser Launch Settings.
* When unchecked, you can choose between:
  * **Default Chrome browser** your installed Chrome is auto-detected and used.
  * **Custom browser** provide a **Browser path** to any Chromium-based browser (Chrome, Brave, Chromium, etc)

{% hint style="success" %}
Not sure where to find your machine's browser path? Open `chrome://version` or `brave://version` in your browser choice and copy the **Executable Path** value.
{% endhint %}

***

**Launch Arguments**

<figure><img src="/files/zWnSzqgpwtbYOjyS3ECJ" alt=""><figcaption></figcaption></figure>

Custom browser executable command line arguments.

* **Use current defaults** inherits from your Browser Launch Settings.
* When unchecked, enter **Chrome command line arguments** as space-separated flags (for example `--mute-audio --disable-notifications`)

{% hint style="warning" %}
Some arguments can interfere with ZeroWork's built-in bot detection bypass and may also prevent the browser from launching. Only add arguments if you know what they do. For a full list of command-line arguments, see [Chromium Command Line Switches](https://peter.sh/experiments/chromium-command-line-switches)
{% endhint %}

***

**Scripts**

<figure><img src="/files/DwoAoOfR1J4wHcvM0txV" alt=""><figcaption></figcaption></figure>

Scripts are injected before any page loads and reinjected on context relaunch.

* **Use current defaults** inherits from your Browser Launch Settings.
* When unchecked, you can add one or more scripts. Each script can be one of:
  * **Path** an absolute file path to a `.js` file (e.g., `/Users/<username>/Documents/script.js`).
  * **Content** inline JavaScript code.
* Click **+ ADD SCRIPT** to add additional scripts. Click the trash icon to remove one.

***

**Page visibility**

<figure><img src="/files/KtppNhMUTa8gN63uRIoY" alt=""><figcaption></figcaption></figure>

**Stay on page after run**

When enabled, the browser stays open after the TaskBot run ends.

{% hint style="warning" %}
**Run in background + Stay on page after run** is not recommended. It leaves an invisible browser consuming resources on your machine after your TaskBot has ended. Additionally, when using [sticky profiles](https://docs.zerowork.io/using-zerowork/using-building-blocks/pages/XWp5BZ8Q3rsexcTMwOM0#id-5.-sticky-browser-profiles-clearprofile-cloneprofile-listprofiles) with this combination, the connection may drop after a long machine sleep, which can block the profile until the Desktop Agent is restarted.
{% endhint %}

#### Things To Know

* **This block always becomes the main browser context.** It replaces any existing browser context. If a [Write JS](/using-zerowork/using-building-blocks/write-javascript) block already launched a context, this block replaces it.
* **Settings become defaults for this run.** Whatever you configure in this block becomes the active defaults for the rest of the run until you change it.
* **There's one browser per sticky profile.** Multiple TaskBots with the same sticky profile ID share the same browser instance in parallel tabs. This block attaches to the existing instance if one is already running. When attaching, browser-level settings (Run in background, Bypass bot detection, Maximize, Window size, Browser, Launch arguments) are ignored as they would've already been set when the browser first launched. Cookies, Scripts, and Page visibility settings still apply.
* **Closing the last tab ends the context.** If a [Switch or Close Tab](/using-zerowork/using-building-blocks/switch-or-close-tab) block closes the last tab, the context ends. The next [Open Link](/using-zerowork/using-building-blocks/open-link) block creates a fresh context from the current defaults.

#### Block Behavior

* **When no browser exists** this block launches a new browser with the configured settings. No URL is opened initially; the browser starts on `about:blank`. Use an [Open Link](/using-zerowork/using-building-blocks/open-link) block after this to navigate to a URL.
* **When a browser already exists (from** [Open Link](/using-zerowork/using-building-blocks/open-link)**,** [Write JS](/using-zerowork/using-building-blocks/write-javascript)**, or a previous Launch Browser block)** this block closes the existing context and launches a new one.
* **When a sticky profile browser is already running from another TaskBot** this block attaches to the existing browser instance. Browser-level settings are ignored. Cookies, Scripts, and Page visibility settings still apply.


# Quit Browser

<figure><img src="/files/c1xa8Ceey7rqDUtOeC9T" alt=""><figcaption></figcaption></figure>

The **Quit Browser** block allows you to close the browser that your TaskBot is using.

{% hint style="success" %}
You might not need this block. If your TaskBot uses a browser, the browser automatically closes when the TaskBot run ends (unless **Stay on page after run** is enabled in your TaskBot's **Browser Launch Settings** or a [**Launch Browser**](/using-zerowork/using-building-blocks/launch-browser) block).
{% endhint %}

#### How To Use It

This block works without configuration, but has one optional setting.

**Force quit**

<figure><img src="/files/ruzF6ckLbP9B7JRRVTVK" alt=""><figcaption></figcaption></figure>

When **Force quit** is checked, the browser instance is forcefully terminated regardless of whether other TaskBots are sharing the same browser instance.

{% hint style="danger" %}
Force quitting a browser context shared with a sticky profile used by other TaskBots will close the sticky profile for all TaskBots using it, not just the currently running TaskBot.
{% endhint %}

#### Things To Know

* **A browser isn't required.** If there's no browser context when this block runs, it logs a warning and continues. Your TaskBot won't fail.
* **Sticky profiles are respected by default.** If other TaskBots are sharing the same sticky profile browser, this block closes your tabs but leaves the browser running for them. The browser only fully quits when the last TaskBot using it leaves.
* **Stay on page after run is overridden.** Even if **Stay on page after run** is enabled in your [Launch Browser](/using-zerowork/using-building-blocks/launch-browser) settings, this block fully closes the browser. An explicit quit takes priority.
* **You can relaunch after quitting.** Placing an [Open Link](/using-zerowork/using-building-blocks/open-link) or [Launch Browser](/using-zerowork/using-building-blocks/launch-browser) block after a Quit Browser block launches a fresh browser context.

#### Block Behavior

* **When a browser exists** the block closes the browser and all its pages.
* **When no browser exists** the block logs a warning and continues.
* **When a sticky profile browser is shared with other TaskBots** the pages being used by your TaskBot are closed, but the browser stays running for other TaskBots.
* **When Force quit is enabled** the browser is terminated regardless of shared instances or **Stay on page after run** settings.


# Switch Frame

The **Switch Frame** building block is here to help you interact with web pages that use iframes. But first, let's understand what an iframe is and why you might need to switch to one.

### What is an iframe?

An iframe (short for inline frame) is a window within a web page that essentially embeds a different page within the main page. Many websites use iframes to load parts of their content separately, like forms, ads, or other embedded content. The challenge with iframes is that they act as a separate browsing context, meaning you can’t interact with the elements inside an iframe directly unless you "switch" into it.

In the example below, you can see a different website embedded into a web page.

<figure><img src="/files/tzZvbXNopoWzQvMzdFxY" alt=""><figcaption></figcaption></figure>

### How to Use the Switch Frame Building Block

1. Identify the selector of the iframe. (More about selectors: [Using Selectors](/using-zerowork/using-selectors).)
2. Select "iframe" in the dropdown and enter your selector.&#x20;

<figure><img src="/files/hrF5lclKQ3TebF1HGh73" alt=""><figcaption></figcaption></figure>

Once you've specified the iframe, TaskBot will enter that context. From now on, all following building blocks will operate within this iframe until you tell TaskBot to switch back.

{% hint style="info" %}
**Tip!** It's recommended to increase selector timeout in the building blocks that rely on selectors (such as *Save Web Element*, *Hover Web Element*, etc.) whenever those actions take place within an iframe. You can find the timeout input field in *Selector Options* directly in those building blocks.&#x20;
{% endhint %}

### Returning to the Main Page

To go back to working on the main page, add another **Switch Frame** block and select "Main frame" in the dropdown. This action makes TaskBot exit the iframe and resume actions on the main page.

<figure><img src="/files/VVlJ4oQ77IvfJzBe0CbR" alt=""><figcaption></figcaption></figure>

### Nested iframes

Some web pages contain iframes inside other iframes. If you need to interact with elements within these layers, you can add several **Switch Frame** building blocks in sequence to navigate to the nested structure. TaskBot will follow each iframe level you define, so you can reach even deeply embedded content.

<figure><img src="/files/t0SNTEwc5rsEvWQ0qc5f" alt=""><figcaption></figcaption></figure>

### FAQ

#### **What happens if the iframe is removed or changed while TaskBot is running?**

If TaskBot cannot find the iframe, it will issue an error and stop.&#x20;

#### What happens if I use the *Open Link* building block to open a new website, or the *Switch Tab* or *Close Tab* building block to activate another tab?

The frame setup will be cleared, as if you had used the *Select Main Frame* action right before the new tab becomes active. *(This handling is available in the upcoming agent 1.1.56.)*

#### How do I get a selector for the iframe or for the element inside it?

Usual ways to obtain selectors continue to work within an iframe. **You do not need any special way to find a selector within an iframe or for the iframe itself**. For example, if you use a selector picker extension to get selectors, just keep using it.

#### Troubleshooting: TaskBot can't click (or perform some other action) the element in the iframe!

The reasons why the element cannot be identified can vary:

* You have not identified the right iframe selector, so your TaskBot is in the wrong iframe.
* Your iframe selector is correct but it is inside another iframe. (See "Nested iframes" example in the section above.)
* You have not identified the right click web element selector.
* Add a **longer selector timeout when performing actions on elements within iframes**. (You can find timeout in Selector Options inside those building blocks.)


# Browser Alert

The **Browser Alert** building block is for handling native browser dialogs like prompts, confirms, alerts, and beforeunload (page exit) confirmations. Per default, TaskBot dismisses these dialogs automatically by clicking **Cancel**. Use this block if TaskBot needs to **accept** the alert.

#### **Prompt text (optional)**

If the browser dialog is a **prompt** requiring input, enter the response text as shown in the screenshot below. This field is ignored for browser dialogs of type alert, confirm or beforeunload (page exit).

<figure><img src="/files/ms9zvtgpLFrjFkkodh9P" alt=""><figcaption></figcaption></figure>

#### **Placement**

Simply place the **Browser Alert** building block after the action that triggers the dialog.

<figure><img src="/files/IZY0o0PDXCXgKP0PzD91" alt=""><figcaption></figcaption></figure>

#### **Run behavior**:

* If a browser alert appears, TaskBot will accept it and continue.
* If no alert is present, TaskBot takes no action and proceeds without interruption.


# Click Web Element

This action clicks on the web element (e.g. button).

For TaskBot to know which element to target, you need to provide a selector (CSS or XPath). Learn about selectors here: [Using Selectors](/using-zerowork/using-selectors).

### Video Tutorial

{% embed url="<https://youtu.be/GLqe4bEVQNM?si=BTgulegKmIZQ17Fa>" %}


# Check Web Element

This action checks if a selector (CSS or XPath) can be found or not.&#x20;

A great example how to use it can be found here: [Send Notification](/using-zerowork/using-building-blocks/send-notification#example-check-if-cookies-expired-or-are-missing).

### Video Tutorial

{% embed url="<https://youtu.be/3a0BSJqVGcg?si=90rB9Vlv1GwseFCX>" %}


# Save Web Element

This action saves public data to a table.&#x20;

### Use Cases

Typically, there are two main categories when data is saved: Saving lists and enriching existing data.

**Examples of saving lists**

* Save a list of profiles from LinkedIn to a table (to contact them later or to move them to your CRM)
* Save FB group members
* Save a list of products in a certain category on Amazon to analyze competitors' prices and new products

See workflow set-up example here: [Save Lists](/using-zerowork/using-building-blocks/save-web-element/save-lists).

**Examples of enriching existing data**

* Have an existing list of LinkedIn profiles and visit every profile to save additional information such as about section, current job position, email address, etc.
* Have an existing list of Amazon products and visit every product page to save description, price and latest customer review.

See workflow set-up example here: [Enrich Existing Data](/using-zerowork/using-building-blocks/save-web-element/enrich-existing-data).


# Save Lists

**Example: Save list of profiles from LinkedIn search results**

In this example, let's assume we want to save 10 profiles from a page.

Here is the workflow set-up:

<figure><img src="/files/M9IA6hqpoi3B1VYMp3kk" alt=""><figcaption></figcaption></figure>

*Open Link* opens the page where the list of profiles is displayed. Note: You should add a url that already contains all necessary search filters, similar to what you see on the screenshot below.

<figure><img src="/files/kprqgJbtcU6E6vx5BDAH" alt=""><figcaption></figcaption></figure>

The loop is set at 10 to be able to save 10 results. The loop is set up in the *Start Repeat* building block.

<figure><img src="/files/rIk9N0iFmQW7F2GlEedt" alt=""><figcaption></figcaption></figure>

*Save Web Element* building block saves profile links like so:

<figure><img src="/files/8L0g8hjNVth9ueDylPxg" alt=""><figcaption></figcaption></figure>

**IMPORTANT**: If you pay close attention to the screenshot above, you will see that the selector contains <mark style="background-color:red;">**{loop\_index}**</mark>. Please read the following section to learn how to add the loop index to your selector (CSS or XPath): [Standard Loop](/using-zerowork/using-building-blocks/start-repeat/standard-loop#how-to-use-loop-index-syntax-to-save-lists-of-public-data).

### FAQ

#### Data is always appended. But I want the new data to overwrite the old data on every run.

Use [Delete Data](/using-zerowork/using-building-blocks/delete-data) building block before you start your loop.

#### How do I get rid of duplicates in my table?

Use [Remove Duplicates](/using-zerowork/using-building-blocks/remove-duplicates) building block after your loop finished saving all the results.

#### How to save paginated results?

Check [Nested Loops - Handle Pagination](/using-zerowork/using-building-blocks/start-repeat/nested-loops-handle-pagination).

#### How to handle scrolling?

Scrolling is handled automatically by your TaskBot.

In-built auto-scrolling logic might fail in some cases. You may need to set up some additional logic by using space or arrow down with a [Keyboard Action](/using-zerowork/using-building-blocks/keyboard-action) block. Contact support to report the website where scrolling is not triggered automatically ([Support](/support/getting-support)).

### Video Tutorial

{% embed url="<https://youtu.be/QNdzT5oIs6g?si=rIcZd5UK6OJ4o-Xs>" %}


# Enrich Existing Data

#### **Example: Visit every profile from a list of LinkedIn profile links and save additional public information such as current job position and email address shown on the profile page**

Here is the workflow set-up:

<figure><img src="/files/Iwj9hlBdckiMcQVDvorM" alt=""><figcaption></figcaption></figure>

The loop is set to iterate over table rows:

<figure><img src="/files/MFEsr4YvfJdENKX8E4XQ" alt=""><figcaption></figcaption></figure>

*Open Link* building block points to the table column "Profile link":

<figure><img src="/files/YKvbfQHtc8wnCZBjEAad" alt=""><figcaption></figcaption></figure>

The two last *Save Web Element* building blocks save job title and about me section:

<figure><img src="/files/E2zbSPIpeaGNWXQNkNnB" alt=""><figcaption></figcaption></figure>

**Table before run**

<figure><img src="/files/YuYYTfrpW29G6Uj8cVfA" alt=""><figcaption></figcaption></figure>

**Table after run**

<figure><img src="/files/8hNWKtjkNx1xNHCemGmK" alt=""><figcaption></figcaption></figure>

#### Avoid visiting same profiles

You should avoid visiting the same profiles. Visiting same profiles over and over slows down your TaskBot runs and data generation. And it might also trigger **anti-bot alerts**. Some websites like LinkedIn track how many profiles you visit per day and block display if you exceed those limits. So do add extra logic to avoid visiting same profiles. The example below explains how to achieve that.

#### **Example: Set profile visit status to "visited" and avoid visiting same profile links by checking if status equals "visited"**&#x20;

Add [Update Data](/using-zerowork/using-building-blocks/update-data) building block:

<figure><img src="/files/F0Cq6qvn9F74oJ6RjQo4" alt=""><figcaption></figcaption></figure>

Create a new column in your table (or in your Google Sheet) and call it "Profile visit status":

<figure><img src="/files/xNTXc16ETRcFxMXsnn0w" alt=""><figcaption></figcaption></figure>

Use *Update Data* building block to mark that a profile was visited like so:

<figure><img src="/files/9bZD8AueLePtEAXnPEvv" alt=""><figcaption></figcaption></figure>

Now you need to set a condition that will check if a profile has already been visited or not. Add [Start Condition and Set Condition](/using-zerowork/using-building-blocks/start-condition-and-set-condition) building blocks like so:

<figure><img src="/files/YmfiGKg8U9dnpS162NfJ" alt=""><figcaption></figcaption></figure>

Your *Start Condition* building block points to the "Profile visit status" column:

<figure><img src="/files/QG1vDLWkQChA6GYov0W8" alt=""><figcaption></figcaption></figure>

And your *Set Condition* building block ensures that TaskBot only opens those links that have not yet been visited:

<figure><img src="/files/uaqjr1EXikMaf7G7m9j9" alt=""><figcaption></figcaption></figure>

The above set-up on how to prevent your TaskBot from visiting same profile links is also demonstrated in a video tutorial:

{% embed url="<https://youtu.be/bDPPa36dvbo?si=IP9HDCTpoZ4k_LYU>" %}

### Video Tutorial

{% embed url="<https://youtu.be/Ozbp1WLtN-4?si=qcSw1QNDdysu250y>" %}


# Insert Text or Data

The **Insert Text or Data** building block inserts text into an input field on a website. It is useful for filling out forms, entering search queries, or automating text input tasks.

For example, you can enter some text as follows:

<figure><img src="/files/8HHHuHqMrLKC852LT6Oa" alt=""><figcaption></figcaption></figure>

By adding the [Open Link](/using-zerowork/using-building-blocks/open-link) building block to open wikipedia.org (as an example) and connecting it to the **Insert Text or Data** building block, you can see how the TaskBot inserts the text into the search field:

<figure><img src="/files/9SCwLmcGtCTnKlzJ15fz" alt=""><figcaption></figcaption></figure>

***

### Options

#### Using a variable or table reference

By clicking on **V** (variable) or **T** (table) at the top right, you can add references to dynamic data.

<figure><img src="/files/wFul00eRvBAjyg4eDNgO" alt=""><figcaption></figcaption></figure>

#### **Insert instantly without typing delay**

The *Insert instantly without typing delay* toggle allows you to paste text instantly without simulating typing.

* When this option is **enabled**, the typing speed is instant and the text is immediately pasted into the input field.
* When this option is **disabled**, the text will be typed character by character at the specified speed. The selector is then optional. If no selector is provided, the text is inserted wherever the mouse cursor is positioned.

#### Using spintax

Spintax lets you vary the content of your text dynamically. Learn more here: [Spintax](/using-zerowork/using-building-blocks/dynamic-inputs/spintax).

<figure><img src="/files/V5K3NhMY8UWmUEmnhFpk" alt=""><figcaption></figcaption></figure>

#### Selector

The behavior of the selector depends on the *Insert instantly without typing delay* setting:

* **When enabled**: A selector (CSS or XPath) is required to specify the input field for pasting text.
* **When disabled**: The selector is optional. If no selector is provided, the text will be inserted wherever the mouse cursor is currently positioned. Some websites automatically place the mouse cursor in the desired input field (e.g., search engines like Google, DuckDuckGo, or Wikipedia). For instance, when you open Wikipedia, the cursor is automatically in the search field, allowing text to be typed directly without needing a selector.

#### Encrypt Content

In some cases, you might prefer logging in to the website/app that you want to automate with your credentials instead of cookies (as described here: [Cookies](/using-zerowork/general-run-schedule-share-webhooks/cookies)). In such cases, you may need to use the Insert Text or Data building block to enter your username and password. If you do so, it is important to encrypt your password to protect your access.

Once encrypted and saved, the content cannot be viewed or edited. To modify it, you must remove the encrypted entry and re-enter your content. We do not store plain text copies of encrypted content.

<figure><img src="/files/JRQHuZe5wJsx5pdeApzx" alt=""><figcaption></figcaption></figure>

***

### FAQ

**Can I Use the Mouse Cursor Instead of a Selector?**

Yes, but only when the option *Insert instantly without typing delay* is **disabled**. In this case, the text is inserted wherever the mouse cursor is currently positioned.

#### When I use a table reference, no data is inserted, although there is data in the table.

This happens when no dynamic loop is set up to pull rows from the table. Please see the solution here: [When I Use Data from Table, No Data is Being Pulled](/using-zerowork/common-problems/when-i-use-data-from-table-no-data-is-being-pulled).

***

### Video Tutorials

See a video tutorial on using **Insert Text or Data** for an automated social media post: [Click Web Element](/using-zerowork/using-building-blocks/click-web-element#video-tutorial).

See another video tutorial on sending a personalized outreach message:[/pages/f1o1B7ewaH1ptL5WMkC3#video-tutorial-connect-your-taskbot-with-open.ai](https://docs.zerowork.io/using-zerowork/using-building-blocks/pages/f1o1B7ewaH1ptL5WMkC3#video-tutorial-connect-your-taskbot-with-open.ai "mention").


# Hover Web Element

This action hovers on a web element.

**Example**

Occasionally, some elements are only shown if hovered. For example, on Facebook Group posts the time of the posting is easier to access on the element that is shown on hover.

Here is an example of a post where the date is shown when you hover on it.

<figure><img src="/files/Xk1Lz9Qls9PFrcvZ2kH9" alt=""><figcaption></figcaption></figure>

In this scenario, you can use this building block to first hover on the post in order to trigger date display and then use *Save Web Element* building block to save the date from the element that is displayed on hover action, as shown below.

<figure><img src="/files/9SWIZKV4DjeGeM6LuvjB" alt=""><figcaption></figcaption></figure>

You need to enter a CSS or XPath selector of the web element as input.

<figure><img src="/files/S2h1ZI3nkVF5SLLdF4bN" alt=""><figcaption></figcaption></figure>


# Select Web Dropdown

This action selects an option in dropdown.

### Selector of Dropdown Element

Correct dropdown selector (CSS or XPath) is critical for this option to work. Your dropdown selector must have tag `select`. For dropdowns with selector tags that are not `select` (for example, `div` or `span`) use [Click Web Element](/using-zerowork/using-building-blocks/click-web-element).

Not all dropdowns will have `select` as their tag. Different websites implement different ways of displaying and handling dropdowns.

#### ✅ Example - valid dropdown

Works here because this dropdown has `select` as its tag.

<figure><img src="/files/AzjZ6JE0P36ax1irxPnA" alt=""><figcaption></figcaption></figure>

For this scenario, you can set up your input this way:

<figure><img src="/files/9T3p8U5RXJLWVEfvhJLx" alt=""><figcaption></figcaption></figure>

Alternatively, you can use variable or table reference, like so:

<figure><img src="/files/GpRbs3hefW6wIdUTmqpM" alt=""><figcaption></figcaption></figure>

#### ❌ Example - invalid dropdown

Won't work because this dropdown does not have `select` as its tag. In this case, 'dropdown' appears visually as a dropdown, but its selector is tagged as a button, so in reality it is a button styled as a dropdown, and you should use [Click Web Element](/using-zerowork/using-building-blocks/click-web-element).

<figure><img src="/files/bIOmqIIXZePsu01i8mAl" alt=""><figcaption></figcaption></figure>


# Keyboard Action

This action hits the keys of a keyboard. The keys can be, for example, Enter, backspace, ->, <-, individual letters or shortcuts such as CMD+C/CTRL+C.

### **Tip 1: Use Keyboard Shortcuts**

Check available shortcuts for your application. By using these shortcuts, you will be able to save a lot of time and skip many building blocks.

Useful resources:

* [Facebook shortcuts](https://www.webfx.com/blog/social-media/facebook-cheat-sheet-of-keyboard-shortcuts/)
* [Slack shortcuts](https://slack.com/help/articles/201374536-Slack-keyboard-shortcuts)
* [Instagram shortcuts](https://greasyfork.org/en/scripts/374589-instagram-keyboard-shortcuts)

### Tip 2: Use Tab

In pretty much every application, you can tab your way through to virtually any element. Using tab can be especially useful when you need to fill out forms.&#x20;

<figure><img src="/files/okjCqjiyqGwnMggBIQLD" alt=""><figcaption></figcaption></figure>

Try it out yourself! Go to <https://www.wikipedia.org/> and start pressing *Tab.* You will see how you can navigate through the options this way. Next, try pressing *Enter* when an option is in focus after tabbing to it - you will see that the element in focus opens. Now, for example, you can count how many tabs you need to get to the option "Wiktionary" - as of today, we needed to press *Tab* 9 times and then press *Enter*.

### Tip 3: Use Shift +  Tab

With Tab you can navigate your way forward and with *Shift + Tab* you can navigate your way ***backwards***. Order matters, so be sure to first add *Shift* and then *Tab* (as opposed to *Tab*, then *Shift*).

<figure><img src="/files/bv5sjpqSiK2QSFSpE4ot" alt=""><figcaption></figcaption></figure>

### Tip 4: Use Escape

Is there a pop-up asking something or a chatbot that pops up and disrupts your TaskBot or a dropdown that needs to be closed before your TaskBot can proceed? Very often, the solution to this problem is to let your TaskBot hit the key *Escape*.

<figure><img src="/files/nB6YLQ03rfX6YDSf1GBV" alt=""><figcaption></figcaption></figure>

### Tip 5: Reload Page

You can use CMD + R (Mac) / CTRL + R (Windows) to reload the page. Note that CMD and CTRL are displayed as Meta in our application.

<figure><img src="/files/D9bS0nG3cUvaFuw2Iy3F" alt=""><figcaption></figcaption></figure>

### Browser Native Commands

ZeroWork can operate within the website context, but not within the native browser commands. So, say, if you want to hit  CTRL/CMD+P (to print something), CTRL/CMD+F (to search for something), CMD+S (to save website as html) or use a browser shortcut command to maximize a window, that won't work.

### Troubleshooting

If you think Keyboard Action building block is not working as expected, please review this page with common issues: [Keyboard Action Is Not Working As Expected](/using-zerowork/common-problems/keyboard-action-is-not-working-as-expected).

### Video Tutorial

{% embed url="<https://youtu.be/gx_qXNK-BDw?si=9oJXqT1hgKIV7cuM>" %}


# Start Condition and Set Condition

*Start Condition* sets the reference that should be used for comparison.

*Set Condition* sets the condition.

### Actions

{% content-ref url="/pages/TNoQ1jxLMZXERD3xoBHF" %}
[Actions = & ≠](/using-zerowork/using-building-blocks/start-condition-and-set-condition/actions-and)
{% endcontent-ref %}

{% content-ref url="/pages/DSe8rCg5baT2xLbVnn6Q" %}
[Actions <, ≤, >, ≥](/using-zerowork/using-building-blocks/start-condition-and-set-condition/actions-less-than-greater-than)
{% endcontent-ref %}

{% content-ref url="/pages/BOpTNmglVuXDELt3ai4n" %}
[Data Found & Data not Found](/using-zerowork/using-building-blocks/start-condition-and-set-condition/data-found-and-data-not-found)
{% endcontent-ref %}

{% content-ref url="/pages/ujSqpFSbEMDtxPIjIQwz" %}
[Contains & Does Not Contain](/using-zerowork/using-building-blocks/start-condition-and-set-condition/contains-and-does-not-contain)
{% endcontent-ref %}

{% content-ref url="/pages/2du2XdSmEgHbJ0lH0H2o" %}
[Before (Date) & After (Date)](/using-zerowork/using-building-blocks/start-condition-and-set-condition/before-date-and-after-date)
{% endcontent-ref %}

### Adding Multiple Conditions

You can add multiple conditions by simply adding multiple *Set Condition* building blocks to the *Start Condition* building block, like so:

<figure><img src="/files/44XXVNOqlkjtoCWGBPRm" alt=""><figcaption></figcaption></figure>

### Chaining Conditions

You can chain multiple *Start* and *Set Condition* building block pairs, like so:

<figure><img src="/files/Ggs1jkgeexj4fkLUkspA" alt=""><figcaption></figcaption></figure>

Also, as you can see above, you can add one *Set Condition* building block for just **one** condition or multiple blocks for multiple conditions.&#x20;


# Actions = & ≠

This condition checks if your data is equal or not equal to a certain value.

Here are workflow set-up examples that use this condition:

* Break loop on condition that status request is unsuccessful, see example here: [APIs: Send HTTP Request](/using-zerowork/using-building-blocks/apis-send-http-request#save-status-response)
* [Enrich Existing Data](/using-zerowork/using-building-blocks/save-web-element/enrich-existing-data#example-set-profile-visit-status-to-visited-and-avoid-visiting-same-profile-links-by-checking-if-sta)(link includes a video tutorial)
* [Delete Data](/using-zerowork/using-building-blocks/delete-data#example-remove-disqualified-leads-if-lead-qualification-status-does-not-equal-qualified) (link includes a video tutorial)


# Actions <, ≤, >, ≥

This condition checks if your data is smaller or greater than (or equal) a certain value.

For example, you can compare number of followers, prices, degrees, etc.

If you need to compare calendar dates, use [Before (Date) & After (Date)](/using-zerowork/using-building-blocks/start-condition-and-set-condition/before-date-and-after-date).


# Data Found & Data not Found

This condition checks if your data is empty or not.

The most common use case for *Data found* or *Data not Found* is to handle edge cases. For example, you can set up this condition to ensure that TaskBot skips those rows that do not contain data in certain columns.


# Contains & Does Not Contain

This condition checks if your data contains or does not contain certain keywords. Separate multiple keywords by comma.

Here is a workflow set-up example: [Update Data](/using-zerowork/using-building-blocks/update-data#example-1-mark-leads-as-qualified-if-their-description-contains-certain-keywords)(link includes a video tutorial)


# Before (Date) & After (Date)

This condition checks if a calendar date is smaller or greater than a comparison date.

#### Days shift

This is a shift added to the date that you enter in the field *Enter days shift*. It can be a positive or negative number.

#### Example: 3 days shift

Image that you entered the comparison date as 10th of July and 3 days shift, as shown below:

<figure><img src="/files/zOd3t5YQAJpGbhxayGrI" alt=""><figcaption></figcaption></figure>

This set-up will convert the comparison date from 10th of July to 13th of July (07/13/2023).

**On&#x20;*****After*****:** The condition will be met if the reference date (the one entered in *Start Condition* building block) is 14th or any day after 14th, which is later than 13th.

**On&#x20;*****Before*****:** The condition will be met if the reference date (the one entered in *Start Condition* building block) is 12th or any day before 12th, which is earlier than 13th.

#### Example: -2 days shift

Negative numbers are accepted, too.&#x20;

<figure><img src="/files/epUBafNDCLhtfHSf7bfW" alt=""><figcaption></figcaption></figure>

This set-up will convert the comparison date from 10th of July to 8th of July (07/08/2023).

**On&#x20;*****After*****:** The condition will be met if the reference date (the one entered in *Start Condition* building block) is 9th or any day after 9th, which is later than 8th.

**On&#x20;*****Before*****:** The condition will be met if the reference date (the one entered in *Start Condition* building block) is 7th or any day before 7th, which is earlier than 8th.

#### Date format

The date format between the comparison date and the reference date must be consistent, otherwise the comparison would be error-prone.

You can change the date format in the date settings as shown below.&#x20;

<figure><img src="/files/0MtaShDGQrWGjvhM4cMw" alt=""><figcaption></figcaption></figure>


# Start Repeat

This is how you can set up a loop to either iterate over rows in your table, save a list of scraped data or repeat some other actions.

### Start and End of the Loop

Loop starts right after *Start Repeat* building block. This means all building blocks that follow *Start Repeat* will repeat for the number of loop iterations.

Loop ends with the last building block.

<figure><img src="/files/7r9NeufOsE4w5pB24Mze" alt=""><figcaption></figcaption></figure>

If you want to make your TaskBot do some actions **after** the loop end, use the building block [After Repeat](/using-zerowork/using-building-blocks/after-repeat).

### Loop Types

There are two loop types. Standard loop saves new data by appending new rows to your table. Dynamic loop iterates over existing rows in your table.

More details and examples here:&#x20;

* [Standard Loop](/using-zerowork/using-building-blocks/start-repeat/standard-loop)
* [Dynamic Loop](/using-zerowork/using-building-blocks/start-repeat/dynamic-loop)

### Options

Your TaskBot can continue where it left off in the previous run. Learn more about it here:

* [Auto-Continue from Last Row or Element](/using-zerowork/using-building-blocks/start-repeat/auto-continue-from-last-row-or-element)

In a standard loop type, your TaskBot can auto-detect the number of elements in a given list. Learn more about it here:

* [Continue until No Element Is Found](/using-zerowork/using-building-blocks/start-repeat/continue-until-no-element-is-found)

### Nested Loops

Nested loops are primarily used to paginate through results. Learn more about it here:

* [Nested Loops - Handle Pagination](/using-zerowork/using-building-blocks/start-repeat/nested-loops-handle-pagination)


# Standard Loop

Standard loop is used to append new rows to your table.

**Suitable for**

* Saving new data - example: scraping a list of profiles from a public website.
* Performing static actions - example: repeatedly pressing keyboard tab key.

**Key characteristics**

* Creates new rows in your table. Creates means appends.
* Can manipulate the data in the rows that are appended during the loop. For example, you can add building blocks like *Format Data* to transform the format of the data that you are saving. Whatever actions your TaskBot takes during a loop iteration, they will all be applied to the same to-be-appended row.
* Ignores any existing data in your tables. If you need to refer to the existing data, use variables, which are loop-independent (or, if applicable, select dynamic loop type, see [Dynamic Loop](/using-zerowork/using-building-blocks/start-repeat/dynamic-loop)).
* Automatically scrolls if the data is displayed in an endless scroll (unless you select auto-detect option, see here: [Continue until No Element Is Found](/using-zerowork/using-building-blocks/start-repeat/continue-until-no-element-is-found)!).

#### Example set-up

See [Save Lists](/using-zerowork/using-building-blocks/save-web-element/save-lists) (link includes video tutorial).

### How to Use Loop Index Syntax to Save Lists of Public Data

Once you review the example above (here is the same link again: [Save Lists](/using-zerowork/using-building-blocks/save-web-element/save-lists)), you will see that the list selector contains <mark style="color:red;">**{loop\_index}**</mark>, so that TaskBot can iterate over items in the list.

Selectors of items in a list contain a number by which they increment, as described here: [Lists: Incremental CSS Selectors](/using-zerowork/using-selectors/how-to-build-custom-selectors/lists-incremental-css-selectors). In place of that number, you need to insert **{loop\_index}**. Same principle applies both to CSS and XPath selectors.

Here is how to do that.

**Step 1**: Simply copy two to three selectors from the list you want to save. As an example, consider these are the first three selectors from the list that you copied.

Original:

* **1st  result  in the list:** main#main li:nth-child(<mark style="color:red;">**1**</mark>) > div.abi-saved-contacts-row\_\_details > button\[type='button'] > div > div > span
* **2nd result in the list:** main#main li:nth-child(<mark style="color:red;">**2**</mark>) > div.abi-saved-contacts-row\_\_details > button\[type='button'] > div > div > span
* **3rd result in the list:** main#main li:nth-child(<mark style="color:red;">**3**</mark>) > div.abi-saved-contacts-row\_\_details > button\[type='button'] > div > div > span

Notice how all the selectors in the list will display an incremental number (highlighted in red above).

**Step 2**: Substitute the incremental number by loop index.

main#main li:nth-child(<mark style="color:red;">**{loop\_index}**</mark>) > div.abi-saved-contacts-row\_\_details > button\[type='button'] > div > div > span

#### Alternative way if you don't know where exactly the increment takes place:

main#main li\~\~<mark style="color:red;">**:nth-child(1)**</mark>\~\~ > div.abi-saved-contacts-row\_\_details > button\[type='button'] > div > div > span <mark style="color:red;">**>> nth={loop\_index}**</mark>

In this case, we are using a filter (>>), as described here: [Combine Filters with Standard CSS Logic](/using-zerowork/using-selectors/how-to-build-custom-selectors/combine-filters-with-standard-css-logic).&#x20;

#### Starting point

Optionally, you can specify your starting point like this: <mark style="color:red;">**{loop\_index,2}**</mark> where 2 means that the count should start from the 2nd element.

Note that the :nth-child() method starts count from 1, whereas the >> nth= method starts count from 0 (0 corresponds to the 1st element in the list, 1 to the 2nd, etc.).

#### Special case: Skip items

In rare cases, a list might skip some numbers. For example, consider this list:

* **1st  result  in the list:** main#main li:nth-child(<mark style="color:red;">**1**</mark>) > div.abi-saved-contacts-row\_\_details > button\[type=“button“] > div > div > span
* **2nd result in the list:** main#main li:nth-child(<mark style="color:red;">**3**</mark>) > div.abi-saved-contacts-row\_\_details > button\[type=“button“] > div > div > span
* **3rd result in the list:** main#main li:nth-child(<mark style="color:red;">**5**</mark>) > div.abi-saved-contacts-row\_\_details > button\[type=“button“] > div > div > span

In this case, you can use this syntax: <mark style="color:red;">**{loop\_index,1,2}**</mark>, where the additional number (in this case it's 2) means that two elements should be always skipped when incrementing the loop index.

#### Advanced use: Referencing the loop index of a parent loop

You can add a loop ID like this: <mark style="color:red;">**{loop\_index\_123}**</mark>, where 123 corresponds to the ID of the Start Repeat building block.

This is useful when you need to reference the loop index of a parent loop. A common use case is when looping through web tables: the parent loop iterates over rows, and the child loop iterates over columns — allowing selectors to access both the parent and child loop indices.


# Dynamic Loop

Dynamic loop is used to iterate over existing rows in a table.

**Suitable for**

* Updating existing data - example: visiting profile links from a table and adding profile details such as "About Me" section to the existing rows or updating their qualification status based on certain conditions.
* Performing actions on existing data - example: visiting profile links from a table and auto-sending outreach messages.

**Key characteristics**

* Iterates over existing rows until it consumes all rows.
* If you don't want to iterate over all rows but need to access, for example, only one row, enter 1 to the optional repetition limit (or any other number that you need).

#### Example set-up

See [Enrich Existing Data](/using-zerowork/using-building-blocks/save-web-element/enrich-existing-data) (link includes a video tutorial).


# Continue until No Element Is Found

When enabled, the loop will continue running until no matching web element is found on the page. This is useful for working with lists of unknown or dynamic length, such as social feeds.

#### **How it works**

If any of the web actions (such as Save Web Element, Click Web Element, Hover Web Element, etc.) returns a selector not found error, the loop will end.

#### Repetition limit is recommended

If your list is long or possible *truly* endless (e.g. a scroll feed), the loop may run for a very long time before reaching the bottom. In these cases, we strongly recommend also setting a repetition limit to prevent long-running or stuck TaskBots.

#### ⚠ Warning

If you enable this option and your loop does not include any web element actions (e.g., Save Web Element, Click Web Element, etc.), the loop will never end. Make sure there is at least one building block that interacts with web elements on the page.

#### Skipping selector not found error

If you are using the Save Web Element building block with the option Skip if no element is found enabled, or if the action is wrapped in a try-catch scope (by using the Start Try-Catch building block), your preference to skip the error will be respected, so the loop won't end when an element is missing.

In such cases, the loop will end only when the scrolling area has reached the end and no further elements can be identified after several attempts. However, if you disable the Auto-scroll option, the loop may run indefinitely — in this setup, TaskBot cannot detect the end of the scrolling area and will assume you are handling scrolling and loop exit manually.

#### **Recommendation**

Leave untoggled if you know how many elements there are on the page. It is more reliable to provide a definite number of repetitions as opposed to using dynamic auto-detection. For example, if you scrape a list of profiles and you know there are always ten profiles per page, then do not toggle this option, simply enter 10 in the number of repetitions field below.


# Auto-Scroll

*This page is upcoming. Stay tuned!*


# Auto-Continue from Last Row or Element

This option lets TaskBot continue where it left off in the previous run. The TaskBot will track the iteration/row number and update it after finishing the loop.

This option is helpful whenever you need to process a large amount data and want to ensure that your TaskBot can process it continually during several subsequent runs. For example, imagine that you are saving a list of 10,000 profiles. On the 1,000th profile the website suddenly crashes. If you toggled *Continue at Latest Iteration*, on its next run TaskBot will start from the 1,001st item and so you don't need to start from scratch.

If you enter a number in *Start from*, the next run will not start at an iteration lower than that value.

This option can be applied both on standard and dynamic loop type. However, it's important to note that there are some differences to how *Continue at Latest Iteration* is used in dynamic versus standard loop types.

### Auto-Continue in Dynamic Loop

In a dynamic loop type, this option refers to the row. So if your TaskBot covered 100 rows during the first run, then on the next run it will start looping from the 101st row.

#### Example: Opening a limited number of profiles per run

You can combine repetition number with this option to only process a limited number of rows per run. An example would be to limit visiting profile links from a table to 100 a day.&#x20;

**Loop Set-up**

Dynamic loop is set up to loop through the table "LinkedIn Profiles". The (optional) repetition limit is set to 100 and the auto-continue option is enabled.

<figure><img src="/files/y8L8vshUFFHd8UMIrMOx" alt=""><figcaption></figcaption></figure>

**How TaskBot Runs**

* **Run 1**: Visits 100 profiles by looping through 1 to 100 rows and updates the continue at latest iteration to 101.
* **Run 2**: Starts from 101st row, loops through 101 to 200 rows and, once finished, updates the continue at latest iteration to 201.
* **Run 3**: Starts from 201st row, loops through 201 to 300 profiles and, once finished, updates the continue at latest iteration to 301.
* ...Runs until the table rows are exhausted.

### Auto-Continue in Standard Loop

In a standard loop type, this option refers to the selector in the list.

**Example: Scraping a large list**

Assume you want to scrape a list of some profiles. A list has 5,000 profiles, and you only want to scrape 100 per day.

**Loop Set-up**

Standard loop is set up to repeat 100 times and has continue at latest iteration option checked.

<figure><img src="/files/IYbp6NcsOs0Ls6gDOHcl" alt=""><figcaption></figcaption></figure>

**How TaskBot Runs**

* **Run 1**: Collects 1 to 100 profiles and updates the continue at latest iteration to 101.
* **Run 2**: Starts from 101st selector, collects 101 to 200 profiles and, once finished, updates the continue at latest iteration to 201.
* **Run 3**: Starts from 201st selector, collects 201 to 300 profiles and, once finished, updates the continue at latest iteration to 301.
* ...Runs until the list is exhausted.


# Nested Loops - Handle Pagination

You can use another *Start Repeat* building block inside of your loop. This is called nested loop (loop inside a loop). You can create as many nested loops within your main or other nested loops as you wish.

Its primary use case is to handle pagination.

#### **Example**: Save LinkedIn profile data from multiple pages - handle paginated results

Imagine you want to save a list of profiles from LinkedIn search results. LinkedIn usually shows 10 profiles per page. This is why you would need to set up a *Start Repeat* building block with a repetition number of 10. See example set-up here: [Save Lists](/using-zerowork/using-building-blocks/save-web-element/save-lists).&#x20;

But how do you make your TaskBot not just save 10 results from one page but go through, for example, 30 pages of results (making it a total of 300 collected profile links)?

For this or any other paginated results, you need a nested loop.&#x20;

Here is the workflow set-up:

<figure><img src="/files/F0qCWegh7qERtnEJiQHi" alt=""><figcaption></figcaption></figure>

*Open Link* opens the page where the list of profiles is displayed.

<figure><img src="/files/W6a1AGhBrcmY7H0bt5qr" alt=""><figcaption></figcaption></figure>

Your main loop needs to loop through 30 pages.

<figure><img src="/files/8fkaZacqVyjjHgmW7oBG" alt=""><figcaption></figcaption></figure>

Your nested loop will loop through profiles and save them, 10 per page.&#x20;

<figure><img src="/files/O3J6lLpJjAxbpnA3GrEe" alt=""><figcaption></figcaption></figure>

After your nested loop finishes saving 10 profiles, main loop continues via *After Repeat* with the action *Click*. Action click will click on the button "Next" and thus paginate to the next page.

<figure><img src="/files/HBlQgdR0cMDe25yjxoCx" alt=""><figcaption></figcaption></figure>

Let's summarize the learnings:

* Loop is set up to repeat 30 times. This corresponds to the number of pages with search results on LinkedIn.
* Within every repetition, your TaskBot enters the nested loop and saves 10 profiles.
* After the nested loop is completed, the TaskBot continues (via [After Repeat](/using-zerowork/using-building-blocks/after-repeat)) with clicking "Next" button to go to the next page. Note that *After Repeat* must be placed after the **nested** *Start Repeat* block.&#x20;
* After that, the cycle repeats: Save 10 profiles, then click on "Next" - for 30 times.&#x20;

### Video Tutorial

{% embed url="<https://youtu.be/CJ2icDqWs9s?si=91KsfkMF5QgYjVG3>" %}


# After Repeat

Lets you do actions **after** loop iterations completed.&#x20;

*After Repeat* building block must always be placed after *Start Repeat* building block. Below are some examples.

#### **Example 1**: Send yourself an email notification after your TaskBot finished its run

This is the workflow set-up where *Start Repeat* building block is set to loop through some actions. *After Repeat* is placed right after *Start Repeat* building block and ensures that TaskBot will send you an email once all loop iterations are completed.

<figure><img src="/files/Cqn2KsebKO6wal5b4Nsb" alt=""><figcaption></figcaption></figure>

#### **Example 2**: Chain multiple loops - first collect leads, then engage with them in the same run

*After Repeat* can produce 'workflow in workflow' effect since it lets you chain multiple independent loops within the same workflow.&#x20;

Imagine that you set up a loop to collect leads, as described here: [Save Lists](/using-zerowork/using-building-blocks/save-web-element/save-lists), and you set up another loop to iterate over collected leads and visit each profile, as described here: [Enrich Existing Data](/using-zerowork/using-building-blocks/save-web-element/enrich-existing-data). You can create two separate TaskBots for these two scenarios. Or, you can use *After Repeat* to unite both in the same workflow, as shown below.

<figure><img src="/files/38Bu1a2MrYykiPMfXRBa" alt=""><figcaption></figcaption></figure>

### Video Tutorial

{% embed url="<https://youtu.be/zUDBdcjxwoc?si=2y9DWsJbdF7Jcb27>" %}


# Break Repeat

This action aborts the current loop. This is used if, for example, the loop should be exited once certain conditions are met.

#### **Example**: Break loop if there are no more profile search results&#x20;

Imagine that you don't know how many pages with search results there are on some page. Once there are no results, the website shows some text like "There are no search results anymore". You can use [Check Web Element](/using-zerowork/using-building-blocks/check-web-element) to check if such text is shown. Then you can set up a condition that if that condition is met, then your TaskBot should stop the loop.&#x20;

<figure><img src="/files/a1DQ6CzXaNgIdg50uiR3" alt=""><figcaption></figcaption></figure>

Another example is described here: Break loop if status request is unsuccessful [APIs: Send HTTP Request](/using-zerowork/using-building-blocks/apis-send-http-request#save-status-response).&#x20;

See a **video tutorial** where Break Repeat is used to exit the loop once a daily DM limit is reached: [Using Variables](/using-zerowork/using-variables#example-use-variable-to-limit-number-of-dms-sent-per-run).


# Run TaskBot

***Available in Agent 1.1.75.***

The **Run TaskBot** block allows you to start another TaskBot from within the currently running TaskBot.

<figure><img src="/files/baWTm4CnNdOgbYP9OiUb" alt=""><figcaption></figcaption></figure>

#### **How To Use It**

* Select the TaskBot you want to run.
* Optionally, pass values to variables in the selected TaskBot. Only the variables you specify are changed. All other variables keep their current values.
* Choose whether the current TaskBot should wait until the selected TaskBot finishes.

#### **Things To Know**

* All variable name and value fields accept [variable and table references](/using-zerowork/using-building-blocks/dynamic-inputs/references-to-variables-and-tables), as well as [expressions or code input](/using-zerowork/using-building-blocks/dynamic-inputs/code-in-inputs).
* You can select **Custom** to provide a TaskBot ID using a [variable or table reference](/using-zerowork/using-building-blocks/dynamic-inputs/references-to-variables-and-tables), [expression/code input](/using-zerowork/using-building-blocks/dynamic-inputs/code-in-inputs), or plain text instead of selecting a TaskBot from the list. This can be useful when building a TaskBot for a client or for sharing with others.
* By default, a value passed to a variable is also saved as that variable's new value in the selected TaskBot. To pass a value for this run only, turn off **Persist value** in the variable's settings inside the selected TaskBot.
* A TaskBot started by another TaskBot can itself start another TaskBot. There is no restriction to one parent-child level.

#### **Block Behavior**

* When **Wait until the TaskBot finishes** is enabled, the current TaskBot pauses until the selected TaskBot finishes. If the selected TaskBot fails or is manually stopped, an error is thrown at this block. You can use [Try-Catch](/using-zerowork/using-building-blocks/try-catch) blocks to handle the error.
* When **Wait until the TaskBot finishes** is disabled, the current TaskBot continues immediately. The selected TaskBot's outcome and errors do not surface in the current TaskBot. You can view them in the selected TaskBot's run reports.
* Recursive behavior is not allowed. For example, if TaskBot A starts TaskBot B and TaskBot B attempts to start TaskBot A, an error is thrown to prevent an infinite loop.
* Manually stopping a TaskBot also stops all child TaskBots started from that run, including child TaskBots further down the chain. For example, if TaskBot A starts TaskBot B, which starts TaskBot C, manually stopping TaskBot A also stops TaskBots B and C. Manually stopping TaskBot B stops TaskBot C, but does not stop TaskBot A. This applies only to child TaskBots started with **Wait until the TaskBot finishes** enabled.


# Try-Catch

You can add any building block — or a sequence of multiple building blocks — after the **Start Try-Catch** block. All of these blocks will be part of the Try-Catch scope.

If an error happens inside this scope, the TaskBot will immediately continue along the **On Catch Error** path, where you can define how to handle the error.

After either the success path or the error path is completed, the TaskBot will continue along the **After Try-Catch** path.

<figure><img src="/files/TuIOcNQ3UzV01pXjxMX4" alt=""><figcaption></figcaption></figure>

### Examples

1. If any error (no differentiation) occurs inside a loop, break the loop and continue after the loop.

<figure><img src="/files/Wmw2JuvXCuZi70vvPSOH" alt=""><figcaption></figcaption></figure>

2. Different try-catch scopes with different error handling strategies for different building blocks.

<figure><img src="/files/M8eyjqQNRrP0YbWjVzOp" alt=""><figcaption></figcaption></figure>

3. Record the error message and handle the error differently depending on the error.

<figure><img src="/files/ZL6h0Zogbluc8UhdEufl" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
If the TaskBot can’t start its run at all, the error won’t be caught — even if you wrap the entire workflow in a try-catch block. This usually happens for one of two reasons:

* Outdated agent version
* Invalid TaskBot set-up&#x20;
  {% endhint %}


# Raise Error

*This page is upcoming. Stay tuned!*


# Abort Run

The **Abort Run** building block stops the TaskBot run immediately.

#### **Example**

It could be useful to abort TaskBot run if a certain condition is met (or not met).&#x20;

For example, if certain critical data is not found or does not exist, you can add **Abort Run** to terminate your TaskBot run.&#x20;

<figure><img src="/files/y75x9uZEvsh384KIimoK" alt=""><figcaption></figcaption></figure>


# Update Data

This action updates data in a variable or table to a new value.

New data can be empty - this will clear the existing data and set it to an empty value.

#### **Example 1: Mark leads as qualified if their description contains certain keywords**&#x20;

Imagine you saved profiles from, for example, LinkedIn to a table called "Linkedin profiles". Now you want to mark all those leads which have keywords "senior", "vp", "director" in their job title as qualified. For this, you can create a column which you can call, for example, "Status" and use this action to update rows with the word "qualified" in the cases where your condition is fulfilled.

This is the workflow set-up:

<figure><img src="/files/7Ag9ELPeUf7pItXhAWGd" alt=""><figcaption></figcaption></figure>

Loop is set up to iterate over profiles in your table:

<figure><img src="/files/xNy7x5yeQMyP2YQC6viw" alt=""><figcaption></figcaption></figure>

*Start Condition* is pointing to the column "Job position".

<figure><img src="/files/M0iWflQBND6W1oDuicIR" alt=""><figcaption></figcaption></figure>

*Set Condition* is checking for the aforementioned keywords.

<figure><img src="/files/PPNoFqRQE8ZZkA1HNBYe" alt=""><figcaption></figcaption></figure>

Now the column "Status" will be set equal to "qualified" as long as the condition above is met.

<figure><img src="/files/6OxsEXffImjdXMPQLXLR" alt=""><figcaption></figcaption></figure>

Later, when you continue working with this table, you can set up your TaskBot to only send connection requests to those contacts who have the word "qualified" in the column "Status" or you can delete all disqualified leads (see example here: [Delete Data](/using-zerowork/using-building-blocks/delete-data#example-remove-disqualified-leads-if-lead-qualification-status-does-not-equal-qualified)).

#### **Example 2: Mark leads who received an outreach message with "Message sent"**

A similar popular use case is to mark a message status as processed. Imagine your TaskBot will be sending out automated messages to a list of profiles. You can create a column called, for example, "Status message" and let your TaskBot update it with "sent" every time a DM is sent. Then by adding a condition to skip sending DMs on profiles who have a record "sent" in their "Status message" column, you can make sure that you never send repeated DMs to the same leads.

<figure><img src="/files/jlyu15D9tVI0l8BrR50W" alt=""><figcaption></figcaption></figure>

#### **Example 3: Set profile visit status to "visited" to avoid visiting same profiles**

Here is a an example of a workflow set-up that checks for status of profile being visited: [Enrich Existing Data](/using-zerowork/using-building-blocks/save-web-element/enrich-existing-data#avoid-visiting-same-profiles) (link includes a video tutorial).

### FAQ

#### When I use a table reference, no data is updated, although there is data in the table.

This happens when no dynamic loop is set up to pull rows from the table. Please see the solution here: [When I Use Data from Table, No Data is Being Pulled](/using-zerowork/common-problems/when-i-use-data-from-table-no-data-is-being-pulled).

### Video Tutorial 1: Qualify Profiles in a Table or Google Sheet

{% embed url="<https://youtu.be/wfN6Oyle8Ic?si=S04AqsnXpaWlcpRW>" %}

### Video Tutorial 2: Use Update Data to Transfer Data from One Table to Another

{% embed url="<https://youtu.be/dQThS6YyHxI?si=WDPvz4gslLUnMtib>" %}


# Number Operations

The **Number Operations** building block allows you to perform various mathematical and formatting operations on numbers. If the value to apply the operation on is empty, the TaskBot will take no action and continue its run.

***

### **Add**

Combine two numbers or concatenate two strings.

* **Examples**&#x20;
  * 1 + 1 ⟶ 2
  * "hello" + " world" ⟶ "hello world"
* **Use** **Case Example**
  * Add a counter to limit number of actions per TaskBot run. See example set-up: [Using Variables](/using-zerowork/using-variables#example-use-variable-to-limit-number-of-dms-sent-per-run)

### **Subtract**

Subtract one number from another.

* **Example**: 5 - 3 ⟶ 2

### **Multiply**

Multiply two numbers.

* **Example**: `4 * 3` ⟶ `12`
* See a use case example here: [Example: Standardize different formats](/using-zerowork/using-building-blocks/number-operations/example-standardize-different-formats)

### **Divide**

Divide one number by another.

* **Example**: 10 / 2 ⟶ 5

### **Remainder %**

Return the remainder after dividing one number by another.

* Example: 13 % 5 ⟶ 3

### **Round**

Round a number to the nearest whole number.

* **Example**: 2.6 ⟶ 3

### **Round up**

Round a number up to the nearest whole number.

* **Example**: 2.2 ⟶ 3

### **Round down**

Round a number down to the nearest whole number.

* **Example**: 2.7 ⟶ 2

### **Get random number**

Generate a random number within a specified range.

If no minimum or maximum values are provided, the TaskBot will use the default values: 0 for the minimum and 1,000,000,000 for the maximum.

* **Example**: Minimum 1, maximum 5 ⟶ 3.45

### **Set decimals**

Set the number of decimal places for a number.

* **Example**: 2 decimals: 5 ⟶ 5.00, 5.5333 ⟶ 5.53

### **Remove formatting**

Remove formatting such as commas or dots in a number.

If a number is used for arithmetic calculations (+, -, /, \*) or numeric comparisons (> or <), it is recommended to first convert it to a clean number for higher reliability. This is when this operation comes in handy.

* **Examples**
  * 1.000.000,23 is dot-grouped and will be converted to 1000000.23
  * 1,000,000.23 is comma-grouped and will be converted to 1000000.23
  * 1,000.000.23 has wrong format and an error report will be generated

***

**Additional Notes**

* If the value to perform the operation on (e.g., value to add to, number to round up) is not provided, the operation is skipped, and the TaskBot will continue without making changes.
* For operations requiring two values (e.g., add, subtract), if the value to use is not provided, the operation is skipped, and the TaskBot will continue without making changes.
* If the operation requires a valid number (any operation except *Add*) and the input is not a number, the TaskBot will generate an error report and stop the run.


# Example: Standardize different formats

Imagine your TaskBots saves number of followers to your table. However, the platform of your choice displays the number of followers in a format like "7k" (which stands for 7000). And this can lead to problems if, for example, you want to use that number for numeric comparisons to qualify only those leads that have a certain amount of followers.

To solve this problem, follow the steps below.

* **Step 1**: Use [Format Data](/using-zerowork/using-building-blocks/format-data) with action [Remove Words](/using-zerowork/using-building-blocks/format-data/remove-words) to remove "k".

<figure><img src="/files/Jpo9vJaeDOQM7SoR3RrA" alt=""><figcaption></figcaption></figure>

* **Step 2**: Use *Number Operations* building block with action *Multiply* to multiply by 1000.

<figure><img src="/files/adDXOAVzfCCazny3EgFe" alt=""><figcaption></figcaption></figure>

### Video Tutorial

{% embed url="<https://youtu.be/iqsvzCPrZT0?si=6xo72Q11lWz0ts7m>" %}


# Format Data

The **Format Data** building block allows you to format text.

If the value to apply the format action on is empty, the TaskBot will take no action and continue its run.

### Actions

* **Remove word**\
  See sub-page [Remove Words](/using-zerowork/using-building-blocks/format-data/remove-words).
* **Replace text**\
  Example: "Hello world" (replace "world" with "universe") ⟶ "Hello universe"
* **Shorten content length**\
  See sub-page [Shorten Content Length](/using-zerowork/using-building-blocks/format-data/shorten-content-length).
* **Convert to lower case**\
  Example: "HELLO WORLD" ⟶ "hello world"
* **Convert to upper case**\
  Example: "hello world" ⟶ "HELLO WORLD"
* **Capitalize first letters**\
  Example: "Hello world" ⟶ "Hello World"
* **Normalize URL**\
  Examples:
  * [www.wikipedia.org](http://www.wikipedia.org) ⟶ <https://www.wikipedia.org/>
  * wikipedia.org ⟶ <https://www.wikipedia.org/>
  * wikipedia ⟶ error is generated "The URL is not a valid URL and cannot be normalized."
* **Trim white spaces**\
  Example: " Hello world " ⟶ "Hello World"
* **Remove line breaks**\
  Example:
  * "Hello\
    world" ⟶ "Hello World"
* **Remove smileys**\
  Example: "Hello world 🌎" ⟶ "Hello World "


# Remove Words

This action removes all entered words (or characters). When entering multiple words, separate them by comma.

#### Example

**Data to be formatted:** AI scientist and PhD candidate Ms. Fina Fedora\
**Words to remove**: Ms., Mr., Mrs., PhD, MBA\
**Result will be:** AI scientist and candidate Fina Fedora (as in "AI scientist and ~~PhD~~ candidate ~~Ms.~~ Fina Fedora")

#### Limitation

Commas cannot be removed (since they are used to separate multiple words). To remove commas, use the action *Replace text* and leave the replacement value empty.

### Video Tutorial

{% embed url="<https://youtu.be/28XdL8KTKfY?si=Id_UHXWJaWKGJviq>" %}


# Shorten Content Length

This action shortens the content length.

**Example of shortening text to 20 characters:**\
"This text needs to be drastically shortened" ⟶ "This text needs to b"

#### Use Case Example

To save your token expenditure when using the [Ask ChatGPT](/using-zerowork/using-building-blocks/ask-chatgpt) building block, you can first shorten the content used for the prompt. See TaskBot set-up here: [Ask ChatGPT](/using-zerowork/using-building-blocks/ask-chatgpt#example-set-up).


# Split Data

The **Split Data** building block allows you to break a value into multiple parts and save them into separate columns or variables.

#### How It Works

* **Value to split** – Select the value that should be split (e.g., a full name or an address).
* **Separator** – Define the character or text that separates the parts (e.g., a space, comma, or custom delimiter).
* **Positions to save** – Specify which parts to extract and where to store them.

#### Example: Split full name to first and last name

Consider this to be your initial table:

<figure><img src="/files/JTGixKYWDO5LM8EsifdW" alt=""><figcaption></figcaption></figure>

How to set up the Split Data building block:

<figure><img src="/files/SrsCcGNR2jxZNDh2bRKW" alt=""><figcaption></figcaption></figure>

This is the end result after running your TaskBot:

<figure><img src="/files/LD2tMJdjbyRcxUlkqYSU" alt=""><figcaption></figcaption></figure>

#### Split from the End of Text

You can enter **negative numbers** in position fields to split from the end of text.

#### **Behavior During Run**

* If the split value **does not contain a requested position**, nothing is saved for that position, and no action is taken.
* **Positive positions** (e.g., `1`, `2`, `3`) start counting from the beginning.&#x20;

  **Examples**:

  * "Hello world", position 1 ⟶ "Hello"
  * "Hello world", position 2 ⟶ "world"
  * "Hello world", position 3 ⟶ *(nothing gets saved)*
* **Negative positions** (e.g., `-1`, `-2`, `-3`) allow counting from the end. *(Supported from the agent version 1.1.61.)*

  **Examples**:

  * "Hello world", position -1 ⟶ "world"
  * "Hello world", position -2 ⟶ "Hello"
  * "Hello world", position -3 ⟶ *(nothing gets saved)*
* **Position 0 is not valid** and does not correspond to any value. TaskBot will generate an error if 0 is used.

### Video Tutorial

{% embed url="<https://youtu.be/iQkygDVEm9Y?si=aZVE5SceNYu0YTJy>" %}


# Apply Regex

The **Apply Regex** building block allows you to perform advanced text formatting and manipulation using regular expressions.

### **Methods**

1. **Replace text**\
   Replaces text matching the given regular expression with a replacement value.
   * **Example 1**: Replace the first occurrence of "birds" with "dogs":\
     **Input Text**: "cats eat birds and birds fly"\
     **Regex**: `/birds/`\
     **Replacement**: "dogs"\
     **Result**: "cats eat dogs and birds fly"
   * **Example 2**: Replace all occurrences of "birds" with "dogs" (using `g`):\
     **Input Text**: "cats eat birds and birds fly"\
     **Regex**: `/birds/g`\
     **Result**: "cats eat dogs and dogs fly"
2. **Extract matches**\
   Extracts text that matches the given regular expression.
   * **Example 1**: Extract the first match of a number:\
     **Input Text**: "Number 123 and there is also another one 345"\
     **Regex:** `/\d+/`\
     **Result**: "123" (Note: without "g", only the first number is extracted.)
   * **Example 2**: Extract all matches of all numbers (using `g`):\
     **Input Text**: "Number 123 and there is also another one 345"\
     **Regex**: `/\d+/g`\
     **Result**: \["123", "345"]
3. **Check if pattern matches**\
   Checks if the text contains a match for the given regular expression and returns `true` or `false`.
   * **Example**: Check if "abc" exists in "abc123":\
     **Regex**: `/abc/`\
     **Result**: "true"

### **Flags**

You can add flags to modify the behavior of your regular expressions.&#x20;

Examples of typically used flags:

* **`g`**: Global search (matches all occurrences).
* **`i`**: Case-insensitive search.
* **`m`**: Multiline search.
* **Example**: `/example/gi` performs a global, case-insensitive search.

### **Use Case Example: Extract Google Sheet IDs**

Imagine you have a list of Google Sheets links and need to extract the IDs.

* Your building block set-up:

<figure><img src="/files/K2Wjjbgm7BN3lVSFkHU2" alt=""><figcaption></figcaption></figure>

* **Result**: Extracted ID is "1aBcD\_12345\_EfGhI67-JklMnOpqrstu890".

<figure><img src="/files/mpNTVPCPKqih94WcszDg" alt=""><figcaption></figcaption></figure>

### **Additional Notes**

* If the input text is empty, the TaskBot will take no action and continue running.
* If your regex is invalid, the TaskBot will generate an error report and stop run/loop.
* You can validate and test your regex using tools like [regex101.com](https://regex101.com/).

### FAQ

#### I'm not a developer, can I still use regex?

Absolutely! Regex is simply a sequence of characters that specifies a match pattern in text. Anyone can use it. There are many reference guides on the internet to learn more. Here is one example: [Regex reference guide](https://learn.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference).


# Remove Duplicates

This action removes all the rows that are duplicate.

{% hint style="warning" %}
It is best practice to always specify a column, unless you have a very special case. Specifying a column will make your TaskBot run faster.
{% endhint %}

Notes: Files in *file* columns will be ignored when searching for duplicates.

The TaskBot will remove **the latest (the newest) rows** and preserve previous rows per default. If you want to reverse this behavior, then check the checkbox *Preserve newest rows* (note that this option is only available in native tables and not available for Google Sheets).

### Do Not Place Inside Loops

This is a batch operation. This means that all table rows - no matter how many there are - are fetched and the duplicate removal is applied to the whole table. Hence, it does not make sense to use this on every loop iteration.&#x20;

{% hint style="warning" %}
If you place this building block inside a loop, duplicates will only be removed once.
{% endhint %}

### Video Tutorial

{% embed url="<https://youtu.be/O9nZUwabzBw?si=A_ILoIOKLtUsf6Gu>" %}


# Delete Data

This action removes individual rows or all data in your table.&#x20;

Below you will learn more about the two available options.

### Delete All Rows

All data is permanently removed.

It's a useful option if you regularly run your TaskBot to collect data and want to overwrite old data with newly collected data on every run. Simply place *Delete Data* building block at the beginning of your workflow before any loops start.

### **Delete One Row**

This option is useful for the case when you want to delete only if a certain condition is met.

#### Example: Remove disqualified leads if lead qualification status does not equal "qualified"

Imagine you collected a list of leads. Your TaskBot updated the column "Qualification" in the table by checking for certain keywords. (There is an example set-up for auto-qualifying leads here: [Update Data](/using-zerowork/using-building-blocks/update-data#example-1-mark-leads-as-qualified-if-certain-conditions-are-fulfilled).) Now every lead that is qualified is marked with the word "Qualified" in the table. Your goal is to delete all the disqualified leads.

This is the workflow set-up:

<figure><img src="/files/8PX1UeE37zUtHRfj3gvI" alt=""><figcaption></figcaption></figure>

The loop is set up to iterate over rows (see [Start Repeat](/using-zerowork/using-building-blocks/start-repeat)).

<figure><img src="/files/3SajgaZs9Ivzz5vTiMXD" alt=""><figcaption></figcaption></figure>

*Start Condition* building block sets the column "Qualification" as the reference.

<figure><img src="/files/Ca0hUFpzuacitAS6lDL2" alt=""><figcaption></figcaption></figure>

Condition in *Set Condition* is set to filter out the qualified leads.

<figure><img src="/files/Vh2gGPyp3codnXjklUdB" alt=""><figcaption></figcaption></figure>

The last building block in this sequence is *Delete Data* and it deletes one row during each loop iteration, as long as the above condition was fulfilled.

<figure><img src="/files/58tuMrNy1r0gXzArken6" alt=""><figcaption></figcaption></figure>

### Video Tutorial

{% embed url="<https://youtu.be/rJupYjpyC1U?si=XKqtf0InBsz10q58>" %}


# Ask ChatGPT

This action gets an answer from ChatGPT for your prompt.

#### Example input

<figure><img src="/files/SOuRBKlYXqqGBGuAGx79" alt=""><figcaption></figcaption></figure>

### Getting API Key

If you see the warning message shown below, it means that you haven't added your OpenAI key.

<figure><img src="/files/Z1EAeCsoDVEM9pcQn1fk" alt=""><figcaption></figcaption></figure>

This is how you can add it.

**Step 1:** Log in to your ChatGPT / OpenAI account at [openai.com](https://openai.com/).

**Step 2:** Open <https://platform.openai.com/api-keys>.

**Step 3:** Create an API key or copy an existing one.

**Step 4**: Open your settings (<https://creator.zerowork.io/settings?tab=1>).

**Step 5**: Add your key here:

<figure><img src="/files/lFYV8ATqT5ClCQ61kixi" alt=""><figcaption></figcaption></figure>

### Customize Behavior on Error

Per default, if OpenAI returns an error, your TaskBot will stop its run (or break the current loop, if the error happens inside a loop).

You can customize this behavior.

**Note**: Some errors - missing prompt, wrong model id and invalid api key - will terminate TaskBot (loop) run, even if you selected continue option.

#### How to customize behavior on error

**Example set-up**

Imagine you asked ChatGPT to create a very long blog article, so that ChatGPT hits its capacity limit and returns a truncated response (you can learn more about truncated responses below). Truncated responses are categorized as errors, so per default TaskBot would stop its run. However, you would like to still save the response - and if it's truncated, you want to save it as a draft.

**Step 1: Change default setting for behavior on error**

Click on the settings gear in *Ask ChatGPT* building block.

<figure><img src="/files/YmIXIrWrFQ7pR8KBIp60" alt=""><figcaption></figcaption></figure>

Then select the option to continue run and record error code.

<figure><img src="/files/isMkFTR0MtYjg648scOj" alt=""><figcaption></figcaption></figure>

**Step 2: Add conditions**

Now you can add a condition that checks if the variable "OpenAI error" contains "length" ("length" is the error code for truncated responses). If it contains "length", then you can make your TaskBot proceed with saving the response as a draft. At the same time, you can also add a second condition which checks if response does not contain the word "length", in which case you can continue with a different path.

<figure><img src="/files/ZUQ0p6aYG4w1eNMtl8hw" alt=""><figcaption></figcaption></figure>

The error status (if any), error code (if provided) and error message (if provided) will be saved to a variable or column, like so: "Status: 400, code: context\_length\_exceeded, message: This model's maximum context length is 4097 tokens. However, you requested 200017 tokens (17 in the messages, 200000 in the completion). Please reduce the length of the messages or completion.". For truncated responses, the error will be recorded as "length".

Here is a list of errors that OpenAI can return: <https://platform.openai.com/docs/guides/error-codes/api-errors>.&#x20;

### Why Responses Can Get Truncated

Every ChatGPT model has a character limit. In ChatGPT, characters are calculated in tokens. So every model has a token limit.&#x20;

For example, for ChatGPT-4, it's 8090, which corresponds to approx. 32,000 characters in English language.

If your prompt asks ChatGPT to generate an answer of more characters than its capacity allows, the responses can get truncated. For example, this prompt "Write an article about marketing of more than 50 000 characters" is likely to result in a truncation error. This can also happen when you use the advanced prompt option called "Max token length" and enter a number that is too low.

Truncation can also happen when your prompt is too long. This is because ChatGPT token limit is shared between the prompt and the answer. For example, this prompt "Here is an article about marketing: \[...pasting an article of more than 50,000 characters...]. Generate a similar one." will result in a truncation error. To avoid this particular problem, you can shorten the length of the dynamic part of your prompt, see section below where an example set-up is shown.

### Token Cost and Saving Strategies

Here is the official price per token page: <https://openai.com/pricing>.

#### **Check token consumption in your TaskBot run reports**

After your TaskBot finishes its run, it will record how many tokens were consumed in your report logs for every request made. Simply open your TaskBot run reports and scroll to the log results of *Ask ChatGPT* building block.

<figure><img src="/files/L0OqPHtRytL6KddzxYKP" alt=""><figcaption></figcaption></figure>

This is how the amount of spent tokens is displayed in the log message:

<figure><img src="/files/m21Mu6zktprIwaF8Eh9p" alt=""><figcaption></figcaption></figure>

#### Shorten dynamic prompt

You can use [Shorten Content Length](/using-zerowork/using-building-blocks/format-data/shorten-content-length) action in *Format Data* building block in order to shorten dynamic content in your prompt. Let's take a look at an example.

#### Example set-up

Imagine that you built a TaskBot that collects social media posts and then asks ChatGPT to generate auto-comments to those posts.

Consider this prompt:

<figure><img src="/files/FtrzUzOCG3tlI2TZUhtN" alt=""><figcaption></figcaption></figure>

You can see that the length in the column "Scraped Post Content" can greatly vary. It's possible that someone shared a very long post of over 1,000 characters. This will consume an unnecessary large amount of your tokens.

A token-saving strategy can be to shorten the post content.

This is how you can do it.

**Step 1**: After having collected the post, use *Format Data* building block to shorten its content to 150 characters (or whatever number you consider adequate).

<figure><img src="/files/OI0Oh6TDsyIwIUFAfTUI" alt=""><figcaption></figcaption></figure>

**Step 2**: To avoid that ChatGPT assumes that we are asking it to complete an incomplete post, let's add a clarification to the prompt, like so:

<figure><img src="/files/Z6V9IMDxC2X5iZoWRa5m" alt=""><figcaption></figcaption></figure>

### Video Tutorial

{% embed url="<https://youtu.be/UXnf8-hy1eQ?si=HQjLkGizU2tozEnB>" %}


# Send Notification

<figure><img src="/files/zarGSvXGMrIiAib81sgZ" alt=""><figcaption></figcaption></figure>

The **Send Notification** block allows you to send yourself an email notification.

{% hint style="danger" %}
The email will be sent to the email address associated with your ZeroWork account. If you need to send an email notification to recipients other than yourself, consider using a [Send HTTP Request](/using-zerowork/using-building-blocks/apis-send-http-request) block to integrate with third-party email APIs.
{% endhint %}

#### How To Use It

The Send Notifiaction block has two fields:

1. **Subject**: the plaintext subject of the email.
2. **Email notification content**: the plaintext or HTML body of the email.

#### Things To Know

* Both fields are required.
* Both fields accept [variable and table references](https://docs.zerowork.io/using-zerowork/using-building-blocks/dynamic-inputs/references-to-variables-and-tables), as well as [expressions or code input](https://docs.zerowork.io/using-zerowork/using-building-blocks/dynamic-inputs/code-in-inputs).

<figure><img src="/files/4umzaOujZICDUgiwKJG3" alt=""><figcaption></figcaption></figure>

#### Block Behavior

* The **Email notification content** field accepts a mix of plaintext and HTML. You may minify HTML in this field to eliminate extra whitespace in the email's body.
* This block has a per minute sending rate limit. The rate limit is automatically applied when this block is activated within a [Start Repeat](/using-zerowork/using-building-blocks/start-repeat) loop.
* Emails are sent from `no-reply@notifications.zerowork.io`. Consider whitelisting this email address within your email client.
* Emails contain a standard footer text.

#### Examples & Use Cases

<div><figure><img src="/files/dK8GuvCU3iNjGFjrRmqR" alt=""><figcaption><p>Notify yourself if an element is missing on a webpage</p></figcaption></figure> <figure><img src="/files/wmR2V22yDqDXw9bmPpFh" alt=""><figcaption><p>Notify yourself if a webpage is down or unreachable</p></figcaption></figure></div>

{% embed url="<https://youtu.be/3a0BSJqVGcg?si=2a0QYUCiY5fRtsXs>" %}


# APIs: Send HTTP Request

This actions makes API requests in your workflow.

To explain how to use it, we will go over a specific example in which we send an API request to open.ai to get answers to questions saved in a table. In this example, open.ai is used to let AI generate answers to pre-defined questions.&#x20;

{% hint style="info" %}
**Note:** The example with open.ai is simply used for demonstration purposes in order to show how to make custom API calls.&#x20;

If you really need to send a request to open.ai, simply use [Ask ChatGPT](/using-zerowork/using-building-blocks/ask-chatgpt) building block which conveniently contains all the configuration options you need. Normally, you **do not need** to use *Send HTTP Request* building block for interacting with open.ai.
{% endhint %}

### Example: OpenAI Final Set-up

In the picture, you can see the final set-up you need to do, and we will go over each option in more detail below:

<figure><img src="/files/rfBp14ZEJKA55XGhhplm" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/cjDUhmsD5zc6ja5nPJ7B" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/nAXy9bmo34iQmeH22hr8" alt=""><figcaption></figcaption></figure>

### Select method

Here you can select different methods (PUT, POST, GET, PATCH, etc.) to create your specific API request.

In the example of open.ai, we need to select POST. To determine what method you need to use, you need to check the documentation or a tutorial page of the API that you are using.

<figure><img src="/files/Q62uOxdrk0sSQ15lwhEo" alt=""><figcaption></figcaption></figure>

### URL

Insert the url that you want to call.

<figure><img src="/files/HUYaOaGQjfkR05RCaYH1" alt=""><figcaption></figcaption></figure>

{% hint style="danger" %}
Beware that some request URLs require slash at the end and some don't. If you add an unnecessary slash, you request may not go through.
{% endhint %}

You can add dynamic data from tables as well. Let's diverge from open.ai for a moment and take another example: Imagine you need to identify a likely age group that your lead or prospect belongs to. For that, you can call agify api. The api url is [https://api.agify.io?name=michael.](<https://api.agify.io?name=michael&#xA;>) As you can see, the name "michael" is dynamic and so you can simply replace that by a table reference. This way in every loop iteration, your TaskBot will make a request with a different name.[<br>](<https://api.genderize.io?name=peter&#xA;>)

<figure><img src="/files/44lkIZcicJuwgYuDO3yg" alt=""><figcaption></figcaption></figure>

### **Headers**

You need to check the requirements of your API what headers you need to add. Often, authorization header is required. For example, in open.ai, you would need to add these two headers:

<figure><img src="/files/wlq3Xd62FO4eJiJBDgEU" alt=""><figcaption></figcaption></figure>

### **Body**

You need to check the requirements of your API if you need to add any body. In open.ai, you would likely need to add the question that you want to get the answer to in your body, and so it would look like this:

<figure><img src="/files/SsvudAp4U7uznOPZEiql" alt=""><figcaption></figcaption></figure>

Note that the body includes a table reference, as you might want to iterate over a list of questions or get the question dynamically from a different source.

### Save Data from Response

Before you hit RUN to call the API, you need to create a table with matching columns. In a moment, you will learn how column matching with API's response keys is done. For now, make sure to create an empty table or use an existing one and then select it in the dropdown *Save response body*, as shown below:

<figure><img src="/files/6RWzvegIQKm3IF9wRDOT" alt=""><figcaption></figcaption></figure>

In case that you do not need to save response - for example, because it's a patch request where you do not expect to receive any response - simply leave that dropdown empty.

In the open.ai example, we want to save the answer and this is why we need to create and select a table (in this example called "Answers").

### Column Matching and Record Path

Your TaskBot will automatically match data from keys to your columns, unless there are nested key-value pairs.&#x20;

For now, let's leave nested key-value pairs aside (we'll get to it in a moment) and assume that your API call returns a simple response with keys on the same level without any nested values like so:

<figure><img src="/files/wAxKK22x9yjlWmmV7Nan" alt=""><figcaption></figcaption></figure>

In this case, you need to create a table that contains columns that you want to save response to, like so:

<figure><img src="/files/pC5X6ZPkpxSyTpOeKKTQ" alt=""><figcaption></figcaption></figure>

Note that this table only contains *name* and *age* but it doesn't contain *count*. This is because you are flexible on what keys to save. So if your table contains columns that are **not** part of the keys in the API response or, like in the example above, does not contain some keys that **are** part of the API response, those will simply be ignored. In other words, you can add *count* if you want or you can omit it if that piece of data does not need to be saved.

After hitting RUN, this is how the data will be populated:

<figure><img src="/files/HzT8PWikGtjdmYIwgDo2" alt=""><figcaption></figcaption></figure>

#### **Nested key-values and record path**&#x20;

Let's get back to the open.ai example. Sample response from open.ai looks like this:

```
{
    "id": "cmpl-GERzeJQ4lvqPk8SkZu4XMIuR",
    "object": "text_completion",
    "created": 1586839808,
    "model": "text-davinci:003",
    "choices": [
        {
            "text": “THIS IS THE ANSWER TO THE QUESTION THAT WE WANT TO SAVE"
            "index": 0,
            "logprobs": null,
            "finish_reason": "length"
        }
    ],
    "usage": {
        "prompt_tokens": 5,
        "completion_tokens": 7,
        "total_tokens": 12
    }
}
```

In this case, we have non-nested and nested values. So if you wanted to save *id*, *object*, *created* and *model,* you would simply create a table with columns named exactly like that, as explained in the example above.&#x20;

But for those key-values that are nested, we need to create a record path like so:

`choices[0]['text']`

Any value inside a list marked by square brackets needs to be accessed with numbers where 0 corresponds to the first item, 1 to the second, etc. placed into square brackets. Any value inside a nested object marked by braces needs to be accessed with its key placed into square brackets.

This record path has to be added extra to the building block like so:

<figure><img src="/files/Ur6DzIVDINpPeoEngsD9" alt=""><figcaption></figcaption></figure>

The nested path also needs to be added as a column name to the table:

<figure><img src="/files/hCjajtPJWDZvwlQFTexJ" alt=""><figcaption></figcaption></figure>

### Save Status Response

You can save the response code to a table. This allows you to recognise errors in the workflow for 400 or 500 codes, so that your TaskBot can handle them accordingly.

<figure><img src="/files/btXT2W31PpOKk141dmDJ" alt=""><figcaption></figcaption></figure>

#### Example: Break loop if status request is unsuccessful

A typical example would be to break the loop in case the status is not equal 200 (200 means success) as shown here:

<figure><img src="/files/ucHIiY6lwZ7PnaTMoik8" alt=""><figcaption></figcaption></figure>

### Invalid JSON Body Error

Beware that json format does not accept double quotes inside the double quotes. See example below.

<figure><img src="/files/qcVMAj4dxH7Z1tyvYPLj" alt=""><figcaption></figcaption></figure>

#### Remove double quotes from dynamic data

Since you may be getting this data dynamically from a website or from a user input (in case you use parameters or table references), you may want to first replace double quotes by single quotes using [Format Data](/using-zerowork/using-building-blocks/format-data) building block and selection the option **Replace text**, as shown below:

<figure><img src="/files/EHHn9nRajQUqZul88YPI" alt=""><figcaption></figcaption></figure>


# Write JavaScript

Use the **Write JavaScript** building block to run custom code with the `zw` API. Your code runs inside the TaskBot runtime and can work with npm packages, variables and tables, state, device storage, and the browser context.

### What You Can Do

#### Imports and Package Management

Install, import, manage, and use npm packages. Use standard `import` statements or `zw.import(package, options)` for custom options.\
**API** `zw.import()`, `zw.packages.*`

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

import dayjs from "dayjs@1.11.11"; // auto-installs if needed
await zw.log("now", dayjs().toISOString());

// Example with options — 'isolate' scopes the package to this TaskBot only
const lodash = await zw.import("lodash@4.17.21", { isolate: true });
await zw.log("chunked result", lodash.chunk([1, 2, 3, 4], 2));
```

**Reference:** [Imports and Package Management](/using-zerowork/using-building-blocks/write-javascript/imports-and-package-management)

***

#### Variables and Tables

Read from and write to variables and tables (native and Google Sheets).\
**API** `zw.getRef()`, `zw.setRef()`

```javascript
const email = await zw.getRef({ ref_id: 3623, name: "Email" });
await zw.setRef({ ref_id: 3624, name: "Email copy", value: email });
```

**Reference:** [Write and Read Variables and Tables](/using-zerowork/using-building-blocks/write-javascript/write-and-read-variables-and-tables)

***

#### Local and Global State

Store and use values for the current run (local state) or across runs (global state). Stays on this device; never sent to ZeroWork servers.\
**API** `zw.state.*`, `zw.globalState.*`

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

// Per-run local state, cleared when the run ends
const runState = zw.state.access();
runState.runStartedAt = Date.now();

// Shared Desktop Agent global state, cleared when Agent quits
const globalState = zw.globalState.access();
globalState.totalRuns = (globalState.totalRuns ?? 0) + 1;
```

```javascript
// In-browser snapshot (survives serialization)
const snapshot = await zw.state.browser.getCopy();
snapshot.runStartedAt = Date.now();
await zw.state.browser.commit(snapshot);
```

**Reference:** [Local and Global State](/using-zerowork/using-building-blocks/write-javascript/local-and-global-state)

***

#### Device Storage

Persist key–value data on this device. Survives restarts and reinstalls. Stays on this device; never sent to ZeroWork servers.\
**API** `zw.deviceStorage.*`

```javascript
const credentials = {
  username: await zw.deviceStorage.get("username_salesforce"),
  password: await zw.deviceStorage.get("password_salesforce"),
};
await zw.deviceStorage.set("last_login_salesforce", String(Date.now()));
```

**Reference:** [Device Storage](/using-zerowork/using-building-blocks/write-javascript/device-storage)

***

#### Utilities

Add delays and write logs.\
**API** `zw.delay()`, `zw.log()`, `zw.logTemp()`

```javascript
await zw.log("starting run");
await zw.delay({ min: 1500, max: 2500 });
```

**Reference:** [Utilities](/using-zerowork/using-building-blocks/write-javascript/utilities)

***

#### Browser and Context

Launch, quit, and manage the active browser context and pages.\
**API** `zw.browserContext.*`

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

// Example (abridged). See the reference for all options.
await zw.browserContext.launch({
  launchConfig: {
    mode: "incognito", // or "sticky"
    bypassDetection: true,
    launchOptions: { headless: true /* proxy, executablePath, ... */ },
    contextOptions: { /* viewport, recordVideo, geolocation, ... */ },
    scripts: [],
    cookies: [],
    onContextReady: async (context) => {
      await context.route('**/*.{png,jpg,jpeg}', route => route.abort());
    },
  }
  // see the reference for the full list of options
});

const contextInfo = await zw.browserContext.getContextInfo();
await zw.log("context", contextInfo);
await zw.browserContext.quit();
```

**Reference:** [Browser Context](/using-zerowork/using-building-blocks/write-javascript/browser-context)

***

#### Metadata

Read TaskBot and ZeroWork Desktop Agent details.\
**API** `zw.getTaskbotInfo()`, `zw.getAgentInfo()`

```javascript
const taskbotInfo = await zw.getTaskbotInfo();
await zw.log("TaskBot name", taskbotInfo.name);
await zw.log("variables", taskbotInfo.variables);

const agent = zw.getAgentInfo();
await zw.log("agent version", agent.version);
```

**Reference:** [Metadata](/using-zerowork/using-building-blocks/write-javascript/metadata)

***

### **Local vs. Browser Execution**

Some methods only work when the code runs locally.

To run code locally, either enable the **Run locally** checkbox in the building block UI or include `// @zw-run-locally` anywhere in your code (top is recommended for visibility).

**Example**

```javascript
// @zw-run-locally
await zw.log("This runs locally now");
console.log("And this will no longer be visible in the browser console.");
```

* **Fully available in browser execution**
  * **Variables and tables** — `zw.getRef()`, `zw.setRef()`
  * **Device storage** — `zw.deviceStorage.*`
  * **Utilities** — `zw.delay()`, `zw.log()`, `zw.logTemp()`
  * **Metadata** — `zw.getTaskbotInfo()`, `zw.getAgentInfo()`
* **Available in browser with limitations**
  * **Imports and package management** — `zw.import()`, `zw.packages.*` \
    Installing and managing packages is only available when running locally; using already imported packages is supported in the browser — see how [**here**](/using-zerowork/using-building-blocks/write-javascript/imports-and-package-management#how-to-use-imported-packages-in-the-browser).
  * **Local and global state** — `zw.state.*`, `zw.globalState.*` \
    Access to the live mutable object via `*.access()` is **only** available locally. In the browser, you can get a snapshot with `*.browser.getCopy()` and save state with `*.browser.commit()` — see how [**here**](https://docs.zerowork.io/using-zerowork/using-building-blocks/pages/DI5u1TjzGfw12nL7h8V2#id-3.-use-state-in-the-browser-zw.state.browser.-and-zw.globalstate.browser).\
    The clear method `*.clear()` is available in both local and browser execution.
  * **Browser context** — `zw.browserContext.*`\
    Largely unavailable in browser execution. Only inspection methods `zw.browserContext.getContextInfo()` and `zw.browserContext.getDefaults()` are supported in the browser.

***

### **Using the `zw` APIs in No-Code Blocks**

You can use the `zw` APIs in no-code blocks, too, either as expressions `${...}` or as code blocks with `return` statements `$${...}`.

**Example**

<div align="left"><figure><img src="/files/cDz3BYs78HKmJahGa77p" alt=""><figcaption></figcaption></figure></div>

**Reference**:  [Code in Inputs](/using-zerowork/using-building-blocks/dynamic-inputs/code-in-inputs)

***

### Error Handling

Thrown errors are automatically saved to **Error Reports** in **Run Reports** (see [Using Run Reports](/using-zerowork/using-run-reports)). Notifications, if enabled, then follow your error run notification settings (by email, Slack, or a custom webhook). This is a useful way to generate custom errors and get notified automatically.

**Example**

```javascript
throw new Error(
  "Test error to verify Error Reports in Run Reports"
);
```

**Resulting error report**

<div align="left"><figure><img src="/files/RRdPa4Oh1QmCqT6rCK7B" alt="" width="563"><figcaption></figcaption></figure></div>

Thrown errors can also be caught by the **Try-Catch** building blocks, see: [Try-Catch](/using-zerowork/using-building-blocks/try-catch).

***

### Notes

* Old API `log()`, `delay()`, `setRef()`, and `getRef()` (without the `zw.` prefix) still work. We recommend the new convention `zw.*` (e.g., `zw.getRef()`), but your current code will continue to run. Existing `activePage` and `taskbotContext` will also continue to work, although they're now discouraged — prefer `zw.browserContext.getActivePage()` and `zw.browserContext.getContext()`.

***

### **Version 1.1.75 revert options** <a href="#version-1.1.75-revert-options" id="version-1.1.75-revert-options"></a>

Version 1.1.75 introduced faster and more reliable browser execution, along with a major bot detection bypass upgrade. See full release notes [here](/release-notes/version-1.1.75). If you notice any unexpected behavior or instability, please file a bug report. In the meantime, you can disable some of these features as follows:

* Add `@zw-revert` to use the previous `zw.*` behavior in browser execution. This does not affect local execution.
* `zw.temp.disableExtraBypass()` disables the additional Bypass Bot Detection improvements.
* `zw.temp.disableExtraHeadlessBypass()` disables the additional headless browser improvements in all browser modes.
* `zw.temp.disableExtraTimezoneBypass()` disables automatic timezone alignment with the configured proxy.

The `zw.temp.*` methods apply only to the current run and must be called again in every TaskBot run where you want them disabled.


# Imports and Package Management

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" to activate' -e 'tell application "TextEdit" to make new document'`);
} 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 lodash from "git+https://github.com/lodash/lodash.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 gitLodash = await zw.import(
  "git+https://github.com/lodash/lodash.git"
);
```

**Never uninstall**

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

#### **Import Option Details**

* **`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

***

### Special Package Types

#### Built-in Packages

These packages 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";
```

#### Pure ESM Packages

ZeroWork isn’t pure ESM yet. **Pure ESM packages** aren’t supported and will throw an error similar to:

> This package appears to be pure ESM. Pure ESM packages are currently not supported. To confirm, check the package documentation.

**Workarounds for pure ESM packages**

* Use a maintained non-ESM fork if available. For example, `cacheable-lookup` is a pure ESM module but a maintained fork is available: `@esm2cjs/cacheable-lookup`.
* Pin a non-ESM version if the project provides one. For example, the latest `chalk` version is pure ESM but its documentation recommends pinning to v4 `chalk@4` for a CommonJS build.
* If the package is pure ESM with no CommonJS fork or version, you will have to find an alternative.

#### Native Packages

Packages that include native code (C/C++ or other system-level bindings) **may not work reliably**. They often require a compatible Node version, build tools, or system libraries. This typically affects modules that talk directly to the operating system or hardware — for example **`node-window-manager`**, **`robotjs`**, or **`ffi-napi`**. Advanced users with the right setup might get them working, but they aren’t guaranteed to install or run on all systems.

***

### 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;

{% hint style="success" %}
You can still ***use*** imported packages in the browser by exposing functions from a local block (see below).
{% endhint %}

### How to Use Imported Packages in the Browser

Import locally in one block, expose a function, then call it 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 %}


# Write and Read Variables and Tables

Read and write persistent values in variables and tables with `zw.getRef()` and `zw.setRef()`.

**API** `zw.getRef()`, `zw.setRef()`

```js
// Get a value from a table column or a variable
const profileName = await zw.getRef({ ref_id: 5079, name: "Profile Name" });

// Save a value to a table column or a variable
await zw.setRef({ ref_id: 4372, name: "Profile Name Copy", value: profileName });
```

***

#### At a Glance

* <mark style="color:$info;">async</mark>\
  `await zw.getRef({ ref_id: number, name: string })` → `string`\
  → returns the `string` value from a variable or table column.
* <mark style="color:$info;">async/sync\*</mark> \
  `await zw.setRef({ ref_id: number, name: string, value: string })` → `void`\
  → saves a `string` value to a variable or table column.

\*`async` in browser, `sync` locally (see [/pages/MF29c3EkhotatZrl92B3#local-vs.-browser-execution](https://docs.zerowork.io/using-zerowork/using-building-blocks/write-javascript/pages/MF29c3EkhotatZrl92B3#local-vs.-browser-execution "mention")).

***

#### What’s `ref_id`?

In ZeroWork, **tables** and **variables** are data groups. Each data group has an ID called `ref_id`.

* **Table `ref_id`** is shown in the left sidebar next to the table name.\
  ![](/files/EPgOAFhjkO7QnaCqF2NU)<br>

* **Variables `ref_id`** is shown in the Variables dialog.

  <figure><img src="/files/2att8rQSEuhNYijBxapq" alt=""><figcaption></figcaption></figure>

* **`name`** identifies a table column or a variable in the Variables group. Names are **case-sensitive**.

  ```javascript
  // Read a table column by its table ref_id and column name
  const profileName = await zw.getRef({ ref_id: 5079, name: "Profile Name" });

  // Same variables ref_id for different variable names
  const date = await zw.getRef({ ref_id: 4379, name: "date" });
  const counter = await zw.getRef({ ref_id: 4379, name: "counter" });
  ```

***

#### Saving Non-String Values

Values are stored as **strings**. To store arrays or objects, **stringify on write** and **parse on read**. Passing a non-string `value` to `zw.setRef()` throws an error.

```javascript
// 🚫 Won't work (value must be a string)
await zw.setRef({ ref_id: 4379, name: "chars", value: ["a", "b", "c"] });
// Error: The value argument must be a string.
```

**Example**

```js
// Save objects or arrays (stringify on write)
await zw.setRef({
  ref_id: 4379,
  name: "animals",
  value: JSON.stringify([{ category: "cat" }, { category: "dog" }]),
});

// Read and use it (parse on read)
const animalsArray = JSON.parse(
  await zw.getRef({ ref_id: 4379, name: "animals" })
);
```

***

#### Loops and Row Behavior

`zw.getRef()` and `zw.setRef()` follow ZeroWork’s loop logic:

* **Read existing rows** → place **Start Repeat** with **dynamic** loop **before** your Write JavaScript building block.
* **Append new rows** → place **Start Repeat** with **standard** loop before the block **or** use a custom append with `appendIndex`.

Read more about how loops work in ZeroWork: [Start Repeat](/using-zerowork/using-building-blocks/start-repeat).&#x20;

**Custom append without Start Repeat**

Use `appendIndex` to add rows from a loop in your code.\
`appendIndex: 0` is the first appended row. If a table already has 3 rows, `appendIndex: 0` becomes the 4th row.

> ⚠️ When using `appendIndex`, placing this Write JavaScript building block inside a Start Repeat loop can mix the built-in loop index with a custom `appendIndex`, causing mismatched rows.

**Example**

```js
const valuesToSave = ["a", "b", "c"];
const TABLE_REF_ID = 4382;

let i = 0;
for (const val of valuesToSave) {
  await zw.setRef({
    ref_id: TABLE_REF_ID,
    name: "value",
    value: val,
    appendIndex: i,
  });
  i++;
}
```

The resulting table:\
![](/files/u21KrkZtKpbfhSCN4eZt)

**Update existing rows in a custom code loop**

Add a **dynamic** Start Repeat building block to iterate rows and use the Write JavaScript building block within that loop. As long as your Write JavaScript is inside that loop, it will have access to the values inside the row that is currently being iterated over. If you need true in-code row access, share your use case in [Discord](https://discord.gg/JkkCNmVBeg) in the #ideas-feature-suggestions channel.

***

#### Common Pitfalls

* **Always use `await` in the browser.**\
  In the browser, `zw.getRef()` and `zw.setRef()` are async — use `await`.&#x20;

  ```javascript
  // 🚫 Won't work (running in the browser)
  const name = zw.getRef({ ref_id: 5079, name: "Profile Name" }); // missing await → name is a Promise, not a string
  await zw.setRef({ ref_id: 4379, name: "Copy", value: name }); // throws error
  // Error: The value argument must be a string.

  // ✅ Works
  const name2 = await zw.getRef({ ref_id: 5079, name: "Profile Name" });
  await zw.setRef({ ref_id: 4379, name: "Copy", value: name2 });
  ```
* **Match the exact name (case-sensitive).**

  ```javascript
  // 🚫 Won't work (case mismatch)
  await zw.getRef({ ref_id: 5079, name: "profile name" });
  // Error: This variable or column does not exist.

  // ✅ Works
  await zw.getRef({ ref_id: 5079, name: "Profile Name" });
  ```


# Local and Global State

Store values for the **current run** (local state) or **across runs on the same Desktop Agent** (global state). Stays on this device; data is never sent to ZeroWork servers.

**API** `zw.state.*`, `zw.globalState.*`

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

// Local per-run state — cleared automatically when the run ends
const runState = zw.state.access();
runState.runStartedAt = Date.now();

// Shared Desktop Agent global state — persists across runs on this device
const globalState = zw.globalState.access();
globalState.totalRuns = (globalState.totalRuns ?? 0) + 1;
```

```js
// In the browser — work with a JSON-safe snapshot of local or global state
const runStateCopy = await zw.state.browser.getCopy();
runStateCopy.lastSeenAt = Date.now();
await zw.state.browser.commit({ state: runStateCopy });
```

***

### 1. Local Per-Run State — `zw.state.*`

A per-run scratchpad automatically initiated on run start. It is **deleted when the run ends**.

#### **At a Glance**

* `zw.state.access()` \
  → returns a **live mutable object** you can read and update anywhere in the run. It can hold anything (e.g., helpers, counters, functions, Maps, Playwright objects like pages or contexts, etc.). It's deleted automatically when the run ends. Not available in the browser (see [**Use state in the browser**](#id-3.-use-state-in-the-browser-zw.state.browser.-and-zw.globalstate.browser) further below).
* <mark style="color:$info;">async/sync\*</mark> \
  `await zw.state.clear()` \
  → clears the whole run state.

\*`async` in browser, `sync` locally (see [/pages/MF29c3EkhotatZrl92B3#local-vs.-browser-execution](https://docs.zerowork.io/using-zerowork/using-building-blocks/write-javascript/pages/MF29c3EkhotatZrl92B3#local-vs.-browser-execution "mention")).

#### **Examples**

**Add a type-safe validator and stringifier around zw\.setRef()**

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

zw.state.access().setRefSafely = ({ ref_id, name, value }, expectedType) => {
  if (typeof value !== expectedType) {
    throw new Error(`Value for ${name} is not ${expectedType}`);
  }
  const valueStr = typeof value === "string"
    ? value
    : (value === null || value === undefined)
      ? ""
      : JSON.stringify(value);
  zw.setRef({ ref_id, name, value: valueStr });
};

// Use the helper in later blocks within the same run
zw.state.access().setRefSafely({ ref_id, name: "age", value: 42 }, "number");
```

**Counters or flags across multiple Write JS blocks in the same run**

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

const runState = zw.state.access();
runState.processed = (runState.processed ?? 0) + 1;
await zw.log("processed", runState.processed);
```

**Clear mid-run if needed**

```js
await zw.state.clear();
```

***

### 2. Global State — `zw.globalState.*`

A device-level scratchpad shared across runs on the same Desktop Agent. Persists between runs until the Desktop Agent quits or restarts. Use it when you need to reuse something across runs on one machine (e.g., cached data, a counter of all runs, etc.).

#### At a Glance

* `zw.globalState.access()` \
  → a **live mutable object** you can read and update across runs. It can hold anything. Not available in the browser (see [**Use state in the browser**](#id-3.-use-state-in-the-browser-zw.state.browser.-and-zw.globalstate.browser) further below).
* <mark style="color:$info;">async/sync\*</mark> \
  `await zw.globalState.clear()` \
  → clears the whole global state.

\*`async` in browser, `sync` locally (see [/pages/MF29c3EkhotatZrl92B3#local-vs.-browser-execution](https://docs.zerowork.io/using-zerowork/using-building-blocks/write-javascript/pages/MF29c3EkhotatZrl92B3#local-vs.-browser-execution "mention")).

#### **Example**

**Track which TaskBots ran today (per device)**

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

const gs = zw.globalState.access();
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD

if (!gs.botsRanToday || gs.botsRanToday.date !== today) {
  gs.botsRanToday = { date: today, bots: [] };
}

const taskbotInfo = await zw.getTaskbotInfo();
if (!gs.botsRanToday.bots.includes(taskbotInfo.id)) {
  gs.botsRanToday.bots.push(taskbotInfo.id);
}
```

#### **Notes**

* It's **not durable** storage. It's **lost when the Desktop Agent quits or restarts**.\
  For durable data use:
  * **Variables/tables** via `zw.getRef()` / `zw.setRef()`
  * **Device storage** via `zw.deviceStorage.*`&#x20;
* As long as the Desktop Agent isn't closed or restarted, global state is **not auto-cleaned**. Clear what you no longer need.

***

### 3. Use State in the Browser — `zw.state.browser.*` and `zw.globalState.browser.*`

In the browser you **cannot** use `*.access()`. Use **JSON-safe snapshots** instead.

#### **At a Glance**

* <mark style="color:$info;">async</mark>\
  `await zw.state.browser.getCopy({ key?: string })` \
  `await zw.globalState.browser.getCopy({ key?: string })` \
  → returns the current state (or the section under `key`).
* <mark style="color:$info;">async</mark>\
  `await zw.state.browser.commit({ state: any, key?: string })`  \
  `await zw.globalState.browser.commit({ state: any, key?: string })` \
  → accepts any **serializable** (JSON-safe) value. If `key` is provided, only that section is updated.

{% hint style="info" %}
`key` is optional and lets you read/update just a section (safer, smaller payloads; no need to snapshot/commit the whole state if it’s large).
{% endhint %}

#### **Examples**

**Whole snapshot**

```js
const stateCopy = await zw.state.browser.getCopy();
stateCopy.step = (stateCopy.step ?? 0) + 1;
await zw.state.browser.commit({ state: stateCopy });
```

**Section-only snapshot with `key`**

```js
const prefs = (await zw.state.browser.getCopy({ key: "prefs" })) ?? {};
prefs.theme = "dark";
await zw.state.browser.commit({ key: "prefs", state: prefs });
```

#### Notes

* **JSON-safe only when using `*.browser.getCopy()` or `*.browser.commit()`.**\
  When using `*.browser.getCopy()` or `*.browser.commit()`, snapshots must be JSON-safe. Functions, Maps, circular references, and similar types cannot be serialized for browser use. If your state contains such values, use `key` to fetch only a JSON-safe section that you need in the browser.
* **In-browser snapshot** **size limits.**\
  While total state size is unlimited, passing snapshots as argument larger than **\~3,000,000 characters** fails. If your state is large, use `key` to work with just a section. Serialization of smaller objects is also faster.
* **In-browser calls require `await`.**\
  In the browser, `*.browser.getCopy()`, `*.browser.commit()`, and `*.clear()` are async.

  ```js
  // 🚫 Won't work
  const stateCopy = zw.state.browser.getCopy(); // missing await
  stateCopy.runData = {}; // throws because stateCopy is a Promise
  await zw.state.browser.commit({ state: stateCopy }); // this line never runs

  // ✅ Works
  const stateCopy = await zw.state.browser.getCopy();
  stateCopy.runData = {};
  await zw.state.browser.commit({ state: stateCopy });
  ```
* **`*.browser.*` scope is reserved for the browser only.**\
  `zw.state.browser.*` is meant for the browser context. In local runs, just use `zw.state.access()` and make a copy yourself if needed:

  ```js
  // @zw-run-locally
  const s = zw.state.access();
  const shallowCopy = { ...s };
  ```


# Device Storage

Persist key–value data on this device. Survives Desktop Agent restarts, uninstalls, and reinstalls. Stays on this device; data is never sent to ZeroWork servers.

**API** `zw.deviceStorage.*`

```js
const credentials = {
  username: await zw.deviceStorage.get("username_salesforce"),
  password: await zw.deviceStorage.get("password_salesforce"),
};
await zw.deviceStorage.set("last_login_salesforce", String(Date.now()));
```

***

#### At a Glance

* <mark style="color:$info;">async</mark>\
  `await zw.deviceStorage.get(key: string)` \
  → returns the `string` value for the key, or `undefined` if not set.
* <mark style="color:$info;">async</mark>\
  `await zw.deviceStorage.set(key: string, value: string)` \
  → saves a `string` value under the key.
* <mark style="color:$info;">async</mark>\
  `await zw.deviceStorage.remove(key: string)` \
  → removes the key and its value from device storage.
* <mark style="color:$info;">async</mark>\
  `await zw.deviceStorage.has(key: string)` \
  → returns `true` if the key exists, otherwise `false`.
* <mark style="color:$info;">async</mark>\
  `await zw.deviceStorage.getAll()` \
  → returns an object of key–value pairs `{ [key: string]: string }`.

***

#### **Adding Secrets**

For passwords, API tokens, and other sensitive values, add them in the Desktop Agent UI instead of saving them as plain text in the **Write JS** building block using `zw.deviceStorage.set()`. (Reading a value with `zw.deviceStorage.get()` is safe.)

To add a secret:

1. Open the Desktop Agent.
2. Select **Device storage** from the tray menu.
3. Click **Add key**, then save your value.

<figure><img src="/files/VU3dFuGO1X3QJC83zMuy" alt=""><figcaption></figcaption></figure>

***

#### **Examples**

**Check if value is present and set if not**

```typescript
if (!(await zw.deviceStorage.has("machine_id"))) {
  await zw.deviceStorage.set("machine_id", "M1234");
}
```

**Securely log in with credentials stored only locally**

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

const credentials = {
  username: await zw.deviceStorage.get("username_salesforce"),
  password: await zw.deviceStorage.get("password_salesforce"),
};

// Get the active page and enter credentials programmatically
// Alternatively, use zw.deviceStorage.* directly in no-code blocks
const page = await zw.browserContext.getActivePage();
await page.goto("https://login.salesforce.com/");
await page.type("#username", credentials.username);
await page.type("#password", credentials.password);
```

**Clear device storage**

```js
// Get an object mapping keys to string values
const all = await zw.deviceStorage.getAll();
await zw.log("deviceStorage keys", Object.keys(all));

const keys = Object.keys(all);
for (const key of keys) {
  await zw.deviceStorage.remove(key);
}
```

***

#### Saving Non-String Values

Device storage only accepts strings. You can **stringify on write** and **parse on read** if you want to work with other types.

```js
// Save an object (stringify on write)
const runMeta = {
  machineId: "M1234",
  lastRunAt: Date.now(),
  lastProcessedInvoiceId: "INV-2026-00127",
};
await zw.deviceStorage.set("billing_bot_state", JSON.stringify(runMeta));

// Read it back (parse on read)
const raw = await zw.deviceStorage.get("billing_bot_state");
const parsed = raw ? JSON.parse(raw) : null;
```

***

### **Logging Secrets**

If you need to log sensitive values (like passwords or API tokens), use `zw.logTemp()` instead of `zw.log()`. `zw.logTemp()` shows the value in the live run logs, but it is **not** saved to TaskBot run reports.

```javascript
const secret = await zw.deviceStorage.get("some_secret");

// ✅ Good: Visible in live logs only (not saved to run reports)
await zw.logTemp(secret);

// 🚫 Avoid: Saved to TaskBot run reports
await zw.log(secret);
```

⚠️ Avoid using `zw.deviceStorage.get()` to read secrets via [Dynamic Inputs](/using-zerowork/using-building-blocks/dynamic-inputs#code-in-inputs) in no-code building blocks. Many building blocks auto-log values, and those logs are saved to TaskBot run reports.

***

#### Notes

* **Size limits.**\
  Total device storage is limited to **\~3,000,000 characters**. Writes that would exceed the total limit will throw an error.
* **Always use `await`.**\
  All methods in `zw.deviceStorage.*` are async, whether the code runs locally or in the browser.&#x20;

  ```js
  // 🚫 Won't work
  const someValue = zw.deviceStorage.get("someKey"); // missing await

  // ✅ Works
  const someValue = await zw.deviceStorage.get("someKey");
  ```


# Utilities

Add delays and write logs.

**API** `zw.delay()`, `zw.log()`, `zw.logTemp()`

```js
await zw.log("Starting run");
await zw.delay({ min: 1500, max: 2500 }); // randomized delay 1.5–2.5s
```

***

#### At a Glance

* <mark style="color:$info;">async</mark>\
  `await zw.delay({ min: number, max?: number })` \
  → pauses for `min` (ms). If `max` (ms) is provided and `max > min`, the delay is randomized between `min` and `max`.
* <mark style="color:$info;">async/sync\*</mark> \
  `await zw.log(...values: any)` \
  → like console.log, logs comma-separated values.
* <mark style="color:$info;">async/sync\*</mark> \
  `await zw.log({ message: any, status?: "success" | "fail" | "warning", tag?: string })` \
  → logs with custom styling.
* <mark style="color:$info;">async/sync\*</mark> \
  `await zw.logTemp(...values: any)` \
  `await zw.logTemp({ message, status?: "success" | "fail" | "warning", tag?: string })`\
  → same as `zw.log()` but not persisted to Run Reports.

\*`async` in browser, `sync` locally (see [/pages/MF29c3EkhotatZrl92B3#local-vs.-browser-execution](https://docs.zerowork.io/using-zerowork/using-building-blocks/write-javascript/pages/MF29c3EkhotatZrl92B3#local-vs.-browser-execution "mention")).

***

#### **Examples**

**Structured status logs (styled in the UI)**

```js
await zw.log({ message: "Logged in", status: "success", tag: "auth" });
await zw.log({ message: "Slow response detected", status: "warning", tag: "network" });
await zw.log({ message: "Failed to submit form", status: "fail", tag: "checkout" });
```

**Log objects, arrays, and functions — no manual stringify needed**

```js
const payload = { id: 42, items: [1, 2, 3] };
const helper = (x) => x * 2;
await zw.log("Logging objects, arrays, functions", payload, helper);
```

**Exact wait with only `min`**

```js
await zw.delay({ min: 1000 }); // exactly 1s, no randomized delay
await zw.log("Waited for exactly 1s");
```

**Dynamic backoff based on state**

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

const state = zw.state.access();
state.retryCount = (state.retryCount ?? 0) + 1;
const backoffMs = Math.min(5000, 500 * state.retryCount);

await zw.log("Retry:", state.retryCount, "waiting:", backoffMs);
await zw.delay({ min: backoffMs });
```

**Live-only logging of secrets (never persisted to Run Reports)**

```js
const token = await zw.deviceStorage.get("auth_token");
await zw.logTemp("Auth token received", token);

// Structured live-only (styled in running log)
await zw.logTemp({ message: token, status: "warning", tag: "debug_auth" });
```

**Try/catch with styled fail log**

```js
try {
  // do something that may throw
} catch (err) {
  await zw.log({ 
    message: `Action failed: ${err?.message}`,
    status: "fail",
    tag: "custom_code_error"
  });
  throw err;
}
```

***

#### Notes

* **Size limits.**\
  The **persistent** message saved to Run Reports is capped at **\~500 characters**. The **running** log view shows up to **\~500,000 characters** per message. Longer inputs do not throw an error but are truncated.
* **Delay always requires `await`.**\
  `zw.delay()` is async and must be awaited in both local and browser execution.
* **Log async behavior.**\
  In the browser, `zw.log()` and `zw.logTemp()` are async — use `await`. When Write JS code runs locally, `await` is optional (the examples use `await` for consistency).


# 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.\
  *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;
  screen?: { width: number; height: number } | null;
  timezoneId?: string;
  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.
* **`screen`**\
  Takes effect when `maximize` is disabled or when `headless` is enabled. Cannot be smaller than viewport.
* **`timezoneId`**\
  Sets the browser context’s timezone. When using a proxy, setting `timezoneId` is especially useful for keeping the browser timezone consistent with the proxy location. If `timezoneId` is not provided and `launchConfig.launchOptions.proxy` is configured, the TaskBot checks the proxy before launch and automatically uses the proxy’s timezone.
* **`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 [/pages/1CmxmmRih9Urxyv0GTdm#local-vs.-browser-execution](https://docs.zerowork.io/using-zerowork/using-building-blocks/write-javascript/pages/1CmxmmRih9Urxyv0GTdm#local-vs.-browser-execution "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;


# Metadata

Read TaskBot and agent details.

**API** `zw.getTaskbotInfo()`, `zw.getAgentInfo()`

```javascript
const taskbotInfo = await zw.getTaskbotInfo();
await zw.log("variables", taskbotInfo.variables);

const agent = zw.getAgentInfo();
await zw.log("agent version", agent.version);
```

***

#### At a Glance

* <mark style="color:$info;">async/sync\*</mark>\
  `zw.getAgentInfo()` \
  → returns `{ version: string, type: "API_KEY" | "DEFAULT" | "GUEST", id: string | null }` (id is `null` if the `type: "GUEST"` ).
* <mark style="color:$info;">async/sync\*</mark> \
  `await zw.getTaskbotInfo()` → returns:

  ```ts
  {
    id: number,
    name: string, // TaskBot name
    runType: "immediate" | "scheduled" | "webhook",
    currentRunResult: "success" | "warning" | "error" | "manually_stopped",
    variables: { ref_id: number, variableNames: string[] },
    tables: Array<{
      ref_id: number,
      name: string,
      type: "G_SHEETS" | "ZW_NATIVE",
      columnNames: string[]
    }>,
    webhookURL: string | null,
  }
  ```

\*`async` in browser, `sync` locally (see [/pages/MF29c3EkhotatZrl92B3#local-vs.-browser-execution](https://docs.zerowork.io/using-zerowork/using-building-blocks/write-javascript/pages/MF29c3EkhotatZrl92B3#local-vs.-browser-execution "mention")).

***

#### **Examples**

**Branch behavior by run type**

```js
const info = await zw.getTaskbotInfo();
if (info.runType === "scheduled") {
  await zw.log("Running in scheduled mode");
}
```

**Get tables by type**

```js
const info = await zw.getTaskbotInfo();
const gSheets = info.tables.filter(t => t.type === "G_SHEETS");
await zw.log("Google Sheets tables", gSheets);
```

**Add webhook URL to state and reuse later**

```js
// @zw-run-locally
import axios from "axios";

// Add TaskBot webhook to global state
const info = await zw.getTaskbotInfo();
const webhookURL = info.webhookURL;
if (webhookURL) {
  (zw.globalState.access().webhooksToTrigger ||=[]).push(webhookURL);
}

// In another TaskBot, access global state and trigger webhooks
const webhooks = zw.globalState.access().webhooksToTrigger || [];
for (const url of webhooks) {
  await axios.post(url);
}
```

***

#### Notes

* **Column/vatiables names, not values.**\
  For tables and variables, metadata returns **names** (variable names and column names). To read values, use `zw.getRef()`.
* **Async behavior.**\
  In the browser, `zw.getTaskbotInfo()` is async — use `await`. When Write JS code runs locally, `await` is optional (examples use `await` for consistency). `zw.getAgentInfo()` is sync everywhere.


# Save File

With the **Save File** building block, you can save file urls or download files.

### File Source

Start by choosing where to get the file from. These are the options:

1. **From file URL**: Use this option if you have a direct URL to the file you want to save.&#x20;
2. **From download action**: This is when your TaskBot performs an action that initiates file download (like clicking a download button).

Let’s go over each option in detail.

***

#### Option 1: From file URL

1. **Enter file URL**: Provide the direct URL to the file. Here is an example of a valid file URL: <https://wallpaperaccess.com/full/2656.jpg>.
2. **Save file to folder**: Enter the folder path where you want the file saved. You can make your TaskBot rename the file in *File options*. This is how to get the folder path: [Upload File](/using-zerowork/using-building-blocks/upload-file#how-do-i-get-folder-path)

<figure><img src="/files/UtQZN8uyYCFyRGr0Epdo" alt=""><figcaption></figcaption></figure>

***

#### Option 2: From download action

1. **Initiate download**: Ensure your TaskBot is set up to first click a download button or take any necessary steps to initiate the download.

<figure><img src="/files/Rd8M3T2DA75Namf6gPCW" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
The system dialog to select folder to save the file to **will not be visible** to you if you watch your TaskBot run. Your TaskBot will handle it 'behind the scenes'.&#x20;
{% endhint %}

2. **Saving Options** (choose one or both):

* **Save file URL to a variable or table**: Select a variable or table to store the file URL from the download action. This can be useful if you want to reuse the file URL in another part of your workflow. For example, you can then use [Upload File](/using-zerowork/using-building-blocks/upload-file) building block and reference to the file URL to get the file from for the upload.&#x20;
* **Save file to local folder**: Enter the folder path to save the file locally. You can make your TaskBot rename the file in *File options*. This is how to get the folder path: [Upload File](/using-zerowork/using-building-blocks/upload-file#how-do-i-get-folder-path)

<figure><img src="/files/jV2DD3eY7flTmTYKaP3O" alt=""><figcaption></figcaption></figure>

***

### File Options

When downloading files to you local folder, you can use **File options** to configure your preferences.

* **Make file name unique on conflict**: Toggle this option if you want to avoid overwriting files with the same name. Whenever there is a file with the same name, your TaskBot will add a random ID to the end of the file name to make it unique (e.g., `file_name_6354736148.png`). If left untoggled, files with the same name in the folder will be overwritten.
* **File name** (optional): Enter a custom file name if you want to rename the downloaded file. If there is already a file with the same name in your folder, then your TaskBot will either make the file name unique or overwrite the file depending on whether or not you toggled the option *Make file name unique*, as described above.
* **Save full file path** (optional): Save the full path of the downloaded file, including the file name, to a table or variable. This can be useful if you want to reuse the file in the [Upload File](/using-zerowork/using-building-blocks/upload-file) building block which can get the file from your folder in order to upload it.

<figure><img src="/files/SOGf6dEQdlo4tMgVM8wQ" alt=""><figcaption></figcaption></figure>

***

{% hint style="danger" %}
**Note**: You can only save your file url or file path to a standard column / variable and cannot save it to a **file** column or variable. This is because *Save File* building block only saves the url or the path to the file whereas the **file** column or variable expects the 'real' file.&#x20;
{% endhint %}


# Upload File

The **Upload File** building block allows TaskBot to upload files from either a direct file URL or a local folder on your computer.

### Step 1: Trigger the Upload Action

Before using the **Upload File** building block, ensure that the upload action is initiated. Typically, you’ll need to add a building block that clicks the upload button, as shown below.

<figure><img src="/files/gxv7VGQ9D7VFHxI8EyDa" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
The system dialog to select files (file chooser) **will not be visible** to you if you watch your TaskBot run. Your TaskBot will handle it 'behind the scenes'.&#x20;
{% endhint %}

***

### Step 2: Select File Source Option

Choose where TaskBot should get the file from. Select one of the following options:

1. **From file URL**\
   If you have a direct URL for the file, enter it in the provided field. This option allows TaskBot to fetch the file from an online source directly.

<figure><img src="/files/G58F2XnEyT1W19O1KUTR" alt=""><figcaption></figcaption></figure>

2. **From folder path**\
   Use this option to upload a file from your local machine. Enter the full file path.

<figure><img src="/files/Q1atigsnWQBTzZogQZBX" alt=""><figcaption></figcaption></figure>

***

### FAQ

#### How do I get folder path?

Here are instructions for Mac, Windows and Linux.

1. On Mac

* Select a folder and perform a **right-click**.
* When the context menu appears, hold the **Option** key on the keyboard.
* **Copy “File-name” as Pathname** option will appear in the context menu. Click it to copy the full file path to the clipboard.

<figure><img src="/files/QEzIgeRteGGiNdCJ32zQ" alt=""><figcaption></figcaption></figure>

2. On Windows

* Open **File Explorer** and navigate to the folder you want.
* Click once in the **address bar** at the top of the File Explorer window. This will show the full path as text.
* Right-click the highlighted path and select **Copy**. You can then paste this path wherever you need.

2. On Linux

* Open the file manager (e.g., Nautilus for GNOME, Dolphin for KDE).
* Navigate to the file you want to use.
* Right-click the folder, select **Properties** or **Get Info**, and copy the **Location** or **Path** shown in the details.

#### Is there a way to add the file manually?

You can add a file column or variable and upload your file to it. Select option *Upload from file URL* in the Upload File building block to reference to that file column or variable.

<figure><img src="/files/Kjbe7mGXu1mv105EwmI6" alt=""><figcaption></figcaption></figure>




---

[Next Page](/llms-full.txt/1)

