Skills DirectorySearch all skills ↗

AI agent skill · by @omnara-ai

Browser Agent

Build and deploy a Browser Agent on Omnara, a long-running agent that signs in to web apps with stored credentials and works in a real browser, overnight if needed. Interviews the user about the job, sign-ins, schedule, and Slack or Discord first. Use when the user asks to build, deploy, set up, update, or remove a browser agent.

Archived 2026-10-23 · Source updated 2026-10-11 · License: Apache-2.0. Check the original repository for the latest version.

SKILL.md instructions

# Build the Browser Agent

You are building an Omnara agent with the user, starting from `agent.yaml` in
this folder. The result: an agent profile that does the user's work in their
web apps, has signed in to each account once while the user watched, and is
reachable where they want it, on a schedule if they want one.

Interview first, then build. Don't create anything in Omnara until the user
has confirmed your summary in step 2.

`agent.yaml` is a guide, not a form. Its browser, sign-in, and machine
settings were tested together: the agent-browser commands, the vault and
two-factor helper in the startup script, the `AGENT_BROWSER_*` variables, and
the settings that keep the machine alive. Keep them unless you have a reason,
and read the file's comments before editing. Fit everything else to the job:
add instruction sections, an MCP server for an API the user already has, or
anything else the job needs, and tell the user what you changed. If a command
fails, read the error and fix it; `npx omnara <command> --help` and
[docs.omnara.com](https://docs.omnara.com) have the details.

The commands use the Omnara CLI (`npx omnara`); add `--json` when you need to
read IDs. The Omnara MCP tools, the
[REST API](https://docs.omnara.com/api-reference/openapi.yaml), and the SDK
work too, and the flags map directly to API fields.

## 1. Connect to Omnara

1. Run `npx omnara whoami`. If it reports that you aren't logged in, run
   `npx omnara login` in an interactive terminal, share the approval link it
   prints, and wait until the user approves.
2. Pick the org and project. `npx omnara whoami --json` lists the user's orgs,
   and `npx omnara projects list --org <org-id> --json` lists an org's
   projects. If there's one of each, use them. Otherwise ask, suggesting the
   defaults from `npx omnara config` or the project named `Default`. Save the
   choice with `npx omnara config --org <org-id> --project <project-id>`.

Remember the project ID; later steps need it.

## 2. Interview the user

Ask these together in one message, in plain language, and say a rough answer
is fine. Don't ask for passwords here; step 4 collects them.

1. **The work.** Is it one set job that runs the same way each time, or a
   helper the team hands different tasks to? For a set job: what it does,
   step by step if there's a set way, what "done" looks like, and what it
   hands back (a message, a CSV, a report). For a helper: the kinds of tasks
   it will get.
2. **Sign-ins.** Which sites does it sign in to? For each: a short name (like
   `crm`), the sign-in page URL, and how it handles two-factor sign-in, if at
   all: an authenticator app, or a code by text or email. Also ask whether any
   two are accounts on the same site, and whether any use SSO (Google, Okta,
   Microsoft).
3. **When it runs.** Whenever someone starts it, or on a schedule such as
   every night at a set time? For a schedule, get the time and timezone.
4. **Where they follow it.** Suggest Slack or Discord, whichever the team
   uses: the team mentions the bot to start a task, and updates, screenshots,
   questions, and results arrive in the thread. The Omnara console always
   works too. Ask whether they want a
   screenshot with every update (the default), only with questions, or only
   with the result.
5. **What it may do on its own.** Overnight no one is around to approve
   anything. Which actions may it take without asking, such as submitting
   forms, sending messages, or deleting records? It will ask about everything
   else and wait.

Follow up only where an answer is too vague to act on, then summarize the
answers in a few lines and get a yes. Explain these where they apply:

- **Authenticator app:** the agent can enter these codes itself from the
  account's setup key, the text code shown under "can't scan the QR code?"
  when the app is set up. If the account already has one, the user may need
  to set it up again to see the key, then add the same key to their phone.
- **Codes by text or email:** the agent asks a person each time it signs in,
  so it can't sign in unattended. Suggest switching that account to an
  authenticator app.
- **SSO:** store the identity provider's sign-in page and credentials. If it
  needs a device prompt or passkey, the agent can't do it alone; suggest
  a hosted browser, as described in `README.md` under "If a site blocks the
  agent".
- **Two accounts on one site:** works, but the agent signs out to switch, so
  it can't use both at once.
- **No sign-ins:** fine; step 4 is skipped.

## 3. Write the job section

Turn answers 1, 4, and 5 into the `{{TASK_INSTRUCTIONS}}` text, using the
user's wording and adding nothing they didn't say. Refer to sign-ins by their
names from answer 2. Put any extra sign-in details that aren't secret, such
as a company ID the form asks for, here too. For a set job:

```text
  Every night, reconcile yesterday's orders in billing against crm.

  1. Sign in to billing and export yesterday's orders as CSV.
  2. Sign in to crm and find the matching deal for each order by order
     number.
  3. Mark the deal "Paid" if the amounts match. List mismatches; don't fix
     them.

  Done means every order is checked. Hand back a summary with counts and a
  CSV of the mismatches. You may update deal status in crm without asking.
  Ask before changing anything in billing.
```

For a helper, describe the work instead of steps:

```text
  The team sends you tasks in the billing and crm web apps: pulling reports,
  looking up records, and updating fields. Confirm what a task needs before
  starting if it's ambiguous. You may update records in crm without asking.
  Ask before changing anything in billing.
```

If the user wants fewer screenshots than the default, add a line such as
"Attach screenshots only to questions." Indent every line by two spaces, as
above, so it nests inside the instruction. Show the text to the user and
adjust it until they're happy.

## 4. Store the sign-ins

For each sign-in, the `LOGIN_KEY` is its name in uppercase with dashes turned
into underscores (`crm` becomes `CRM`, `ops-portal` becomes `OPS_PORTAL`).
Recommend accounts made for the agent, with only the access the job needs,
since everything it does appears under that account.

1. Check for existing secrets named `browser-agent-<name>-username`,
   `-password`, and `-totp`:
   `npx omnara secrets list --owner-kind project --owner-project-id <project-id> --name <secret-name> --json`.
   If they exist, ask whether to reuse them. To change a value, have the
   user update it on the **Secrets** page of the Omnara dashboard; the ID
   stays the same.
2. Ask the user to put the missing values in a `.env` file in the current
   directory and tell you when it's saved. Discourage pasting passwords into
   the chat. `TOTP` is the authenticator setup key, only for sites that use
   one. The file is read by the shell, so wrap each value in single quotes;
   otherwise spaces, `$`, or `#` in a value break it. Write a single quote
   inside a value as `'\''`.

   ```sh
   LOGIN_CRM_USERNAME='agent@acme.com'
   LOGIN_CRM_PASSWORD='...'
   LOGIN_CRM_TOTP='JBSW Y3DP EHPK 3PXP'
   ```

3. Create a secret for each value without printing it, and note each `id`
   (`sec_…`):

   ```sh
   set -a && . ./.env && set +a
   npx omnara secrets create --owner-kind project --owner-project-id <project-id> \
     --name browser-agent-crm-password --material-kind generic \
     --material-value "$LOGIN_CRM_PASSWORD" --json
   ```

## 5. Pick the model, machine pool, and machine size

1. `npx omnara grant models list --json` lists `model.provider_config` and
   `model.name` pairs. The agent looks at screenshots, so the model must
   accept images. Suggest `anthropic/claude-opus-5.5` on `omnara-openrouter`
   if it's granted, otherwise the strongest image-capable model listed.
2. `npx omnara grant pools list --json` lists the pools as
   `machine_pool.name`. Suggest `default-pool` if it's granted; its machines
   have the Node.js 24 the startup script needs. Otherwise use the only pool,
   or ask. A custom pool's image needs Node.js 24, npm, and root for
   installing Chrome's system packages. On a Daytona, Modal, or Tenki pool,
   delete the `sleep_after_ms` line; those providers don't support it.
3. Use `8192` MB for `MACHINE_MEMORY_MB`, or the largest
   `max_machine_memory_mb` the pool and its grant allow if lower (both are in
   the `grant pools list` output). Chrome needs memory, and on Blaxel the file
   system is in memory too. If the user needs more than the pool allows, they
   can raise its limits on the **Machines** page.

## 6. Build the config

1. Copy `agent.yaml` from this folder to `./browser-agent.yaml`. If this
   folder isn't available locally, download
   https://raw.githubusercontent.com/omnara-ai/omnara/main/examples/browser-agent/agent.yaml
   instead.
2. Replace `{{TASK_INSTRUCTIONS}}` with the text from step 3.
3. Fill in the sign-ins. Each one is a `LOGIN_<KEY>_URL` line under
   `env_overlay` and its secrets under `secret_env_overlay`, plus
   `LOGIN_<KEY>_TOTP` if it has a setup key. With two sign-ins:

   ```yaml
   env_overlay:
     LOGIN_CRM_URL: "https://crm.acme.com/login"
     LOGIN_BILLING_URL: "https://billing.acme.com/signin"
     # AGENT_BROWSER_… lines unchanged
   secret_env_overlay:
     LOGIN_CRM_USERNAME: "sec_…"
     LOGIN_CRM_PASSWORD: "sec_…"
     LOGIN_CRM_TOTP: "sec_…"
     LOGIN_BILLING_USERNAME: "sec_…"
     LOGIN_BILLING_PASSWORD: "sec_…"
   ```

   With no sign-ins, delete the `LOGIN_` lines and the empty
   `secret_env_overlay:` key.
4. Leave the machine settings alone, except: if any site sends codes by text
   or email, set `sleep_after_ms: 0`, since a sign-in after waking would need
   a person. The machine is kept until the agent is archived
   (`delete_after_idle_minutes: 0`), so its browser and sign-ins carry over
   between tasks. It sleeps after two hours with no command running
   (`sleep_after_ms: 7200000`) and wakes in under a second with Chrome,
   memory, and files intact; a working agent runs commands every few
   seconds, so it never sleeps mid-task.
5. Fill in `{{MACHINE_MEMORY_MB}}`, `{{MODEL_PROVIDER_CONFIG}}`,
   `{{MODEL_NAME}}`, and `{{MACHINE_POOL}}` from step 5.
   `grep -n '{{' browser-agent.yaml` must print nothing.
6. Create or update the profile, named `browser-agent` unless the user wants
   another name. Check with `npx omnara profiles list --name browser-agent --json`:
   - None: `npx omnara profiles create --name browser-agent --file ./browser-agent.yaml --json`
   - Exists: `npx omnara profiles update <agent-profile-id> --file ./browser-agent.yaml --json`

   If it rejects the reasoning effort, the model doesn't support `high`:
   remove the `reasoning` block and retry. Note the profile's `id` and
   `current_config_id`.

## 7. Test it

Launch a first task that changes nothing:

```sh
npx omnara agents launch --profile <agent-profile-id> --config <current-config-id> \
  --message "Don't start your job yet. Sign in to each of your saved sign-ins, and for each one tell me what's on the first page you land on, with a screenshot." --json
```

Give the user the link to watch:
`https://app.omnara.com/projects/<project-id>/agents/<agent-id>`. The first
reply takes a few minutes while the machine installs agent-browser and
Chrome. With no sign-ins, ask it to open a site from the job instead.

If a sign-in fails, read the agent's messages before changing anything. The
usual causes are a sign-in URL that redirects elsewhere (store the URL the
form actually lives on) or a site that blocks headless browsers from
datacenter IPs (see "If a site blocks the agent" in `README.md`).

Keep this agent if the user wants a schedule without Slack or Discord; step 9
uses it. Otherwise archive it in the console once every sign-in works.

## 8. Connect Slack or Discord (if chosen)

**Slack.** If `npx omnara integrations list --json` already has an integration
named `browser-agent-slack` from an earlier run of this skill, reuse it whatever
its state, since names are unique in a project. If
`npx omnara integrations get <integration-id> --json` doesn't list this profile
under `settings.launcher.profiles`, add it with
`npx omnara integrations profiles <integration-id> --profile-ids <agent-profile-id>`;
the flag replaces the list, so repeat each profile already there. Then, if its
`state` is `active`, skip to its last step. If it's `disconnected`, setup didn't
finish: skip step 2, and redo step 1 only if the token in `.env` has expired.

1. The user creates an app configuration token at
   [api.slack.com/apps](https://api.slack.com/apps) under **Your App
   Configuration Tokens** → **Generate Token**. It expires after about 12
   hours. Have the user add it to `.env` as `SLACK_APP_CONFIG_TOKEN=...`.
2. Create the integration with this profile in its launcher, so a mention
   starts this agent, and note the `itg_…` ID it returns:

   ```sh
   npx omnara integrations create --name browser-agent-slack \
     --integration-kind slack_thread \
     --settings '{"launcher":{"profiles":["<agent-profile-id>"]}}' --json
   ```

   The name is permanent and names the agent's Slack tools
   (`int__browser-agent-slack__post_message`).
3. Create the Slack app and connect it:

   ```sh
   set -a && . ./.env && set +a
   npx omnara integrations slack <integration-id> --app-name "Browser Agent" \
     --app-configuration-token "$SLACK_APP_CONFIG_TOKEN"
   ```

   Ask before choosing a different app name; it's how the team mentions the
   bot. It opens a Slack authorization page that the user approves before it
   expires.
4. Tell the user to invite the bot to a channel (`/invite @Browser Agent`). A
   private channel suits it, since its updates show data from the sites.

**Discord.** The bot answers in a new thread wherever it's mentioned in a
server channel; it doesn't answer direct messages.

If an integration named `browser-agent-discord` already exists, reuse it the
same way: add this profile if it's missing. If it's `active`, skip to step 5;
Omnara can't tell whether steps 5 and 6 were done in Discord, so check them with
the user. If it's `disconnected`, skip step 3 and do the steps that weren't
done, passing the current `setup_revision` from
`npx omnara integrations get <integration-id> --json` in step 4.

1. The user creates an application named "Browser Agent" in the
   [Discord Developer Portal](https://discord.com/developers/applications);
   ask before choosing a different name, since it's how the team mentions
   the bot. They copy the **Application ID** and **Public Key** from
   **General Information**. Under **Bot**, they select **Reset Token** to copy
   the bot token, turn on **Message Content Intent**, and turn off **Public
   Bot** so only they can add it (if Discord refuses, first set
   **Installation** → **Install Link** to **None**). Have the user add the
   values to `.env`:

   ```sh
   DISCORD_APPLICATION_ID=...
   DISCORD_PUBLIC_KEY=...
   DISCORD_BOT_TOKEN=...
   ```

2. Store the bot token and note the `sec_…` ID. If a secret named
   `browser-agent-discord-bot-token` already exists
   (`npx omnara secrets list --owner-kind project --owner-project-id <project-id> --name browser-agent-discord-bot-token --json`),
   reuse its ID; if the user has reset the token since, have them paste the new
   one into it on the dashboard's **Secrets** page (the ID stays the same).
   Otherwise:

   ```sh
   set -a && . ./.env && set +a
   npx omnara secrets create --owner-kind project --owner-project-id <project-id> \
     --name browser-agent-discord-bot-token --material-kind generic \
     --material-value "$DISCORD_BOT_TOKEN" --json
   ```

3. Create the integration with this profile in its launcher, and note the
   `itg_…` ID and `setup_revision` it returns:

   ```sh
   npx omnara integrations create --name browser-agent-discord \
     --integration-kind discord_thread \
     --settings '{"launcher":{"profiles":["<agent-profile-id>"]}}' --json
   ```

   The name is permanent and names the agent's Discord tools
   (`int__browser-agent-discord__post_message`).
4. Connect the bot. Omnara checks the token and finds the bot's user:

   ```sh
   set -a && . ./.env && set +a
   npx omnara integrations configure <integration-id> \
     --expected-setup-revision <setup-revision> \
     --provider-tenant-id "$DISCORD_APPLICATION_ID" \
     --credential-secret-id <secret-id> \
     --provider-config "{\"public_key\":\"$DISCORD_PUBLIC_KEY\"}" --json
   ```

5. Under **General Information**, the user sets **Interactions Endpoint URL**
   to `https://app.omnara.com/api/integrations/discord/<application-id>/interactions`
   and saves; the buttons on the bot's questions need it. Discord checks the
   URL when it's saved, so this has to come after step 4.
6. The user adds the bot to their server by opening this link, which asks
   only for the permissions the bot needs:
   `https://discord.com/oauth2/authorize?client_id=<application-id>&scope=bot&permissions=309237746752&integration_type=0`.
   A private channel suits it, since its updates show data from the sites;
   the user adds the bot to it under the channel's **Permissions**.

Each mention in a new thread starts a new agent with its own machine, and
replies in the thread go to the same agent. Kept machines count toward the
pool's machine limit, so tell the user to archive a thread's agent in the
console when its work is done.

## 9. Schedule it (if chosen)

A schedule sends each run's message to one standing agent, so every run uses
the same machine and starts signed in:

- **With Slack or Discord:** have the user mention the bot once in the
  channel where the results should go, for example "@Browser Agent this
  thread is for your nightly runs; reply 'ready' and wait". Get that agent's
  ID from the conversation's URL in the console.
- **Without either:** use the agent from step 7.

Convert the time and timezone from answer 3 to a five-field cron expression:

```sh
npx omnara crons create --name browser-agent-nightly \
  --target-type agent --target-agent-id <agent-id> \
  --cron '0 22 * * *' --timezone America/Los_Angeles \
  --message-template 'Start your job for tonight.' --json
```

If `npx omnara crons list --name browser-agent-nightly --json` already shows a
trigger, use `npx omnara crons update <cron-trigger-id>` instead. If a run is
still going when the next fires, the message waits in the queue. Don't target
the profile, and don't use the Slack or Discord integration's **Add
schedule**: both start a new agent, with a new kept machine, every run.

## 10. Wrap up

Summarize what you created: the secrets, the profile, and the Slack or Discord
integration and schedule if any. Then tell the user:

- **Machines are kept** until their agent is archived, sleeping when idle and
  waking with the browser intact. Archive agents whose work is done.
- **To change anything,** ask a coding agent with this skill. It reuses the
  secrets and updates the profile. Running agents keep the config they
  started with, so for a scheduled agent, start a new standing agent, point
  the trigger at it with `npx omnara crons update`, and archive the old one.
- **To rotate a password,** update its secret on the **Secrets** page, then
  start a new agent the same way; running machines keep the old value.
- **To remove it,** delete the cron trigger (`npx omnara crons delete <id>`),
  delete the Slack or Discord integration
  (`npx omnara integrations delete <integration-id>`), or, if other agents
  share it, rerun `npx omnara integrations profiles` with only their
  profiles. Then run `npx omnara profiles delete <agent-profile-id>` and
  `npx omnara secrets delete <secret-id>` for each secret.
  With Discord, also delete the application in the Discord Developer Portal.

Related skills