DEV Community

Cover image for Route Browser Automation Tasks by Account Context, Not Worker Availability
web4browser
web4browser

Posted on

Route Browser Automation Tasks by Account Context, Not Worker Availability

Most browser automation queues start simple.

A task enters the queue.
A worker becomes available.
The worker opens a browser.
The script runs.

That model works when every task is anonymous.

It starts breaking when the browser is already logged in.

Here I am talking about authorized team workflows: internal dashboards, QA accounts, support operations, client-approved workspaces, or other account-bound browser tasks where the team is allowed to operate those accounts.

In these workflows, the browser is not just a runtime. It carries account state, cookies, local storage, proxy routing, region settings, permissions, and sometimes a human review history.

If your scheduler treats every browser profile as a generic worker slot, the queue can quietly send a task into the wrong account environment.

That is how a Playwright script can be technically correct and still operate in the wrong place.

For teams building long-running account workflows, the missing layer is not another retry loop. It is account-aware task routing. A browser automation workspace should know which account context a task belongs to before it lets a worker touch a page.

The common worker pool model is too generic

The examples below are pseudo-TypeScript. The goal is to show the routing model, not a complete queue implementation.

A basic browser automation task often looks like this:

type BrowserTask = {
  taskId: string;
  url: string;
  action: "inspect" | "click" | "extract" | "submit";
  payload?: Record<string, unknown>;
};
Enter fullscreen mode Exit fullscreen mode

Then the worker pool does something like this:

const task = await queue.next();
const worker = await workers.firstAvailable();

await worker.run(task);
Enter fullscreen mode Exit fullscreen mode

This is fine for public page checks, static scraping, QA smoke tests, and other jobs where identity does not matter.

It is weak for logged-in workflows.

The task does not say which account owns the work.
It does not say which browser profile must be used.
It does not say which proxy group is expected.
It does not say whether human review is required before submit actions.
It does not say what should happen if the original profile is busy.

The scheduler only sees “available worker”.

The business workflow actually needs “the correct account environment”.

Account context should be part of the task

For logged-in browser automation, the queue payload should include the account context contract.

A more useful task model looks like this:

type AccountBoundBrowserTask = {
  taskId: string;
  accountId: string;
  profileId: string;
  proxyGroupId: string;
  expectedRegion: string;
  action: "inspect_status" | "collect_metrics" | "prepare_form";
  targetUrl: string;
  riskLevel: "low" | "review_required" | "blocked";
  createdBy: "human" | "agent" | "schedule";
};
Enter fullscreen mode Exit fullscreen mode

Now the scheduler has something to enforce.

The worker should not ask:

Which browser is free?
Enter fullscreen mode Exit fullscreen mode

It should ask:

Which worker can run this exact account context safely?
Enter fullscreen mode Exit fullscreen mode

That difference matters.

A profile with the right cookies but the wrong proxy is not valid.

A profile with the right proxy but the wrong account session is not valid.

A profile that is already locked by another task is not valid.

A profile that requires human review before a submit action is not valid for a fully automated run.

Match workers by environment, not availability

Account affinity means a task should prefer, or require, a specific account environment.

This is similar to routing distributed jobs to nodes with the right data locality. In browser automation, the “data” is the account state inside the browser profile.

A simple routing rule can look like this:

function canRunTask(worker: BrowserWorker, task: AccountBoundBrowserTask) {
  return (
    worker.profileId === task.profileId &&
    worker.proxyGroupId === task.proxyGroupId &&
    worker.region === task.expectedRegion &&
    worker.status === "available"
  );
}
Enter fullscreen mode Exit fullscreen mode

Task assignment becomes stricter:

async function assignTask(task: AccountBoundBrowserTask) {
  const candidates = await workers.list();

  const worker = candidates.find((candidate) =>
    canRunTask(candidate, task)
  );

  if (!worker) {
    await queue.defer(task, {
      reason: "no_matching_account_context",
    });
    return;
  }

  await worker.lockProfile(task.profileId, task.taskId);
  await worker.run(task);
}
Enter fullscreen mode Exit fullscreen mode

This is not just a performance detail. It is a correctness rule.

If the correct account context is unavailable, the safe behavior is to wait, defer, or ask for review.

The unsafe behavior is to run the task in a profile that looks “similar enough”.

Run a preflight check before page actions

Even when the scheduler selects the right worker, the worker should still verify runtime state before acting.

A preflight check can inspect:

  • Current profile ID
  • Expected account ID
  • Proxy group or outbound region
  • Login state
  • Current domain
  • Permission level for the requested action
  • Whether the profile is locked by another run
  • Whether the task requires human review

A preflight result might look like this:

{
  "task_id": "task_1042",
  "account_id": "acct_store_us_018",
  "profile_id": "profile_us_018",
  "expected_region": "US",
  "actual_region": "US",
  "login_state": "active",
  "profile_lock": "owned_by_current_task",
  "permission": "inspect_only",
  "preflight_status": "pass"
}
Enter fullscreen mode Exit fullscreen mode

If the check fails, the worker should stop before performing the business action.

const preflight = await runPreflight(task);

if (preflight.status !== "pass") {
  await evidence.write({
    taskId: task.taskId,
    stopReason: preflight.reason,
    expected: preflight.expected,
    actual: preflight.actual,
  });

  throw new Error(`Preflight failed: ${preflight.reason}`);
}
Enter fullscreen mode Exit fullscreen mode

A failed preflight is not a crash.

It is the system preventing an account-context mistake before it becomes a business mistake.

Lock the profile during execution

Profile locks prevent two workers from using the same logged-in environment at the same time.

Without a lock, this can happen:

  1. Worker A opens Profile 018.
  2. Worker B also opens Profile 018.
  3. Worker A changes state.
  4. Worker B reads an old assumption.
  5. Both write different evidence.
  6. The team cannot tell which run caused the final state.

A basic profile lock can include:

type ProfileLock = {
  profileId: string;
  taskId: string;
  workerId: string;
  lockedAt: string;
  expiresAt: string;
};
Enter fullscreen mode Exit fullscreen mode

The lock needs a TTL because workers can crash.

The worker should refresh the lock while the task is running. If the worker dies, the lock eventually becomes stale and can be reviewed or released.

Do not silently steal locks unless the task is clearly safe to resume.

In account workflows, a stale lock often means the previous browser state is unknown. That should trigger inspection before retry.

Retries must preserve account ownership

Retries become risky when they ignore account context.

This is a bad retry rule:

If task fails, send it to any available worker.
Enter fullscreen mode Exit fullscreen mode

This is safer:

If task fails, retry only in the same account context unless a human approves reassignment.
Enter fullscreen mode Exit fullscreen mode

Retry metadata should preserve ownership:

{
  "retry_of": "task_1042",
  "allowed_profile_id": "profile_us_018",
  "allowed_proxy_group_id": "proxy_us_stable",
  "retry_reason": "navigation_timeout",
  "max_attempts": 2,
  "requires_review_after": 2
}
Enter fullscreen mode Exit fullscreen mode

Some failures are safe to retry, such as a temporary navigation timeout before login state is touched.

Some failures should stop immediately:

  • Account mismatch detected
  • Region mismatch detected
  • Unexpected verification prompt
  • Submit action blocked by policy
  • Profile lock conflict
  • Page shows a different account identity than expected

A good automation system does not retry everything.

It classifies failures before deciding whether to continue.

Log expected context against actual context

When a browser task runs in a multi-account system, logging only “success” or “failed” is not enough.

The log should compare expected context with actual runtime context:

{
  "run_id": "run_20260615_001",
  "task_id": "task_1042",
  "worker_id": "worker_07",
  "expected_account_id": "acct_store_us_018",
  "actual_account_marker": "store-us-018",
  "expected_profile_id": "profile_us_018",
  "actual_profile_id": "profile_us_018",
  "expected_proxy_group": "proxy_us_stable",
  "actual_proxy_group": "proxy_us_stable",
  "preflight_status": "pass",
  "final_status": "completed"
}
Enter fullscreen mode Exit fullscreen mode

This makes debugging much easier.

If the task fails, the team can see whether the script logic failed or whether the task entered the wrong environment.

That distinction matters.

A selector timeout is a script issue.
A profile mismatch is a routing issue.
A proxy mismatch is an environment issue.
A submit blocked by review policy is a permission issue.

Without context logs, all of these look like “automation failed”.

A practical routing checklist

Before scaling browser automation across many logged-in profiles, check these rules:

  • Every task has an accountId.
  • Every account maps to one expected profileId.
  • Every profile maps to an expected proxy group or region.
  • Workers cannot run account-bound tasks without passing preflight.
  • Profiles are locked during execution.
  • Retries preserve account ownership.
  • Review-required tasks cannot run as fully automated submit actions.
  • Logs compare expected context with actual runtime context.
  • Failure reasons separate script errors from environment errors.
  • Stale locks require inspection before release.

This checklist changes how the whole system behaves.

The scheduler stops treating browsers as disposable execution slots.

It starts treating each browser profile as an account-bound runtime.

Final thought

In simple automation, any browser can be a worker.

In multi-account automation, the browser profile is part of the task identity.

That means routing matters as much as scripting. A correct Playwright script can still produce the wrong result if it runs inside the wrong account context.

The safer model is to bind each task to its account, profile, proxy group, permission level, and evidence trail before execution starts.

That is the direction tools like Web4 Browser are moving toward: a multi-account browser workbench where profiles, proxies, AI tasks, logs, and human review are handled as one workflow instead of scattered settings.

Top comments (0)