If you have ever automated a third-party portal, you have probably hit the same wall: the thing works in staging, runs fine for a few days, then fails because a button moved, a modal appeared, a session expired, or the site shipped a minor frontend change that broke your selector. The hard part is not clicking the button once. The hard part is making the workflow boring enough to run hundreds or thousands of times.
There are three common layers where people run this kind of automation: browser agents, deterministic browser scripts, and network replay. They can all work. They fail in different ways.
Browser agents are the fastest way to get a first result
A browser agent drives the UI the way a person would. It reads the page, reasons about the next action, clicks, types, waits, and adapts when the layout changes.
That makes it useful when you know almost nothing about the target system. If an internal ops team says, "Log into this payer portal, find this patient, download the eligibility result," an agent can often attempt that without a developer reverse-engineering the site first.
The cost is predictability. Every page transition can involve model calls, screenshots, DOM extraction, or both. Latency tends to be measured in seconds or minutes, not milliseconds. Failures are also harder to classify because the runtime is making judgment calls.
A typical failure looks like this:
Agent stopped: unable to determine next action
Last observation: "Your session is about to expire"
Current goal: "Submit enrollment form"
A human would click "Continue" and move on. The agent may or may not, depending on whether it recognizes the interruption and whether its instructions allow it.
This layer is still the right starting point for unknown portals, desktop apps, Citrix sessions, and workflows where no stable web API exists. Just do not confuse "it completed once" with "it is production-ready."
Deterministic browser scripts reduce cost, but inherit UI fragility
Once you understand a workflow, you can replace agent reasoning with a script. That usually means Playwright, Puppeteer, Selenium, or a vendor-generated equivalent.
import { chromium } from "playwright";
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("https://example-portal.com/login");
await page.fill('input[name="username"]', process.env.PORTAL_USER!);
await page.fill('input[name="password"]', process.env.PORTAL_PASS!);
await page.click('button[type="submit"]');
await page.waitForURL("**/dashboard");
await page.fill('input[aria-label="Patient ID"]', "123456");
await page.click('text=Search');
await page.click('text=Download PDF');
await browser.close();
This is cheaper and easier to debug than a full agent loop. You can log every step, retry specific operations, and write tests around known states.
But it still depends on the rendered UI. If the portal changes input[aria-label="Patient ID"] to input[placeholder="Member ID"], Playwright will fail with something like:
TimeoutError: locator.fill: Timeout 30000ms exceeded.
Call log:
- waiting for locator('input[aria-label="Patient ID"]')
You can make scripts less brittle with better locators, explicit waits, screenshots on failure, and fallback selectors. You cannot remove the basic dependency: the UI is now part of your integration contract, even though the site owner never promised to keep it stable.
Network replay is usually the production layer
Most web portals are frontends over HTTP APIs. When you click "Search," the browser often sends a JSON request to a private endpoint. If you can identify that request and reproduce it safely, you can skip the UI entirely.
In Chrome DevTools, this usually starts in the Network tab:
POST https://example-portal.com/api/member/search
content-type: application/json
cookie: session=...
{ "memberId": "123456" }
Then you test whether the call works outside the browser:
curl 'https://example-portal.com/api/member/search' \
-H 'content-type: application/json' \
-H "cookie: session=$PORTAL_SESSION" \
-d '{"memberId":"123456"}'
If that returns the same data, you have a much better automation path. No selectors. No screenshots. No waiting for animations. No LLM deciding whether a modal matters.
The tradeoff is setup work. You need to understand auth, CSRF tokens, pagination, rate limits, request signing, and any backend validation the frontend hides. Some portals make this easy. Others rotate tokens, bind requests to browser fingerprints, or put meaningful state in opaque headers.
A network-layer system such as Wire packages that discovery behind catalog actions, so callers invoke an action_id instead of maintaining private endpoint details themselves.
The API shape often looks like an async job because the provider handles login, retries, and target-site latency behind the scenes:
curl -X POST https://api.openwire.sh/v1/wire/task \
-H "X-API-Key: $ANAKIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action_id": "amazon.search_products",
"params": { "query": "wireless keyboard" }
}'
{
"status": "processing",
"job_id": "3f8aa45d-6ea3-4107-88ce-7f39ecf48a84",
"poll_url": "/v1/wire/jobs/3f8aa45d-6ea3-4107-88ce-7f39ecf48a84"
}
That pattern matters more than the specific vendor. You submit a semantic action, poll by job ID, and receive structured data. Your code depends on a schema, not on a button label.
The schema is part of the product, even for internal tools
Developers often focus on getting the data once, then regret not defining the output contract. If one run returns price, another returns amount, and a third returns pricing.current, your downstream pipeline now has to guess.
For production workflows, treat the response shape like any other API contract:
{
"products": [
{
"title": "Logitech MX Keys",
"price": 99.99,
"currency": "USD",
"rating": 4.7
}
]
}
Validate it at the boundary:
import { z } from "zod";
const Product = z.object({
title: z.string(),
price: z.number(),
currency: z.string(),
rating: z.number().nullable()
});
const SearchResult = z.object({
products: z.array(Product)
});
const parsed = SearchResult.parse(apiResponse.data);
This catches drift early. Without validation, the first symptom may be a bad report, a failed billing job, or an empty CSV sent to a customer.
Catalog-based tools like Wire are strongest when the action has a documented input and output schema that stays stable across callers.
How to choose the layer
Use a browser agent when you need a first result on an unknown system, especially if the workflow spans desktop software, virtualized apps, or messy human-oriented screens.
Use a deterministic browser script when the workflow is known, volume is moderate, and UI changes are acceptable operational risk.
Use network replay when the workflow is high-volume, latency-sensitive, or feeding another system that expects consistent structured data.
The practical next step: open DevTools on one workflow you currently automate through the browser, perform the action manually, and inspect the Network tab. If the real operation is a small JSON request, move that path out of the UI before you spend more time making selectors prettier.
Top comments (0)