Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Build browser automation as a state machine with an explicit human handoff state. Let Playwright (or another controller) handle deterministic navigation, stop before MFA, CAPTCHA, sensitive data, ambiguous choices, messages, payments, or irreversible submissions, and show the operator the exact action about to run. The operator approves, edits, or cancels it in the same live session. When control returns, re-read the page, verify that the intended state still exists, record the decision, and only then continue.

What human-in-the-loop automation means

Human-in-the-loop (HITL) automation is not a script that occasionally asks for a password. It is a workflow in which human review is a defined, auditable state. Cloudflare describes the pattern as letting a person step into a live browser session through a live view, handle what automation cannot, and hand control back to the script.

The browser session, cookies, page state, and pending task remain alive during the handoff. The person sees the same page the controller sees, completes or corrects the risky step, and returns control without starting over.

When to pause

  • MFA, SSO, CAPTCHA, device checks, or other proof that only the account owner should provide.
  • Credentials, payment details, health information, identity documents, or other sensitive data.
  • Complex one-off interactions where the agent cannot establish the correct target.
  • Sending a message, approving an order, changing permissions, downloading a sensitive file, or submitting an irreversible form.
  • Any page state that conflicts with the plan, such as a changed price, recipient, account, or quantity.

A safe architecture

Separate planning from execution and make the policy gate authoritative. The agent may propose an action, but it must not bypass a gate that marks the action as requiring approval.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Planner or agent: interprets the user’s goal and proposes the next browser action in structured form.
  2. Policy gate: classifies the action. Require approval for credentials, payments, messages, downloads, privilege changes, and irreversible submissions.
  3. Browser controller: executes routine actions with Playwright. Playwright drives Chromium, Firefox, and WebKit through one API and is intended for testing, scripting, and AI agents.
  4. Handoff view: exposes the same live session in a controlled browser window, remote desktop, or managed workspace. Freeze agent commands while the operator has control.
  5. Decision record: stores the proposed action, origin, relevant fields, operator identity, decision, and timestamp.
  6. Resume check: re-read visible state after takeover. Never assume that the DOM, recipient, price, or target remained unchanged.
  7. Audit and recovery: retain permitted screenshots or traces, provide cancel and rollback paths, and route uncertain outcomes to review.

Bind approval to the executable action

Do not display a generic “Continue?” prompt assembled from page text. Untrusted page content can influence an approval dialog. The Verifiable Action Card paper evaluates 24 scenarios, including confused-deputy attacks, approval-dialog forgery, indirect prompt injection, action substitution, provenance evasion, and legitimate tasks. Your approval record should identify the exact operation that will execute.

Field Example Why it matters
Action type submit_order Prevents a page from replacing an approved operation with another one.
Origin https://shop.example Shows which site and account context are involved.
Target fields Item, quantity, total, recipient Lets the operator verify consequential values.
Session and task IDs Random identifiers Stops an approval from being replayed in another task.
Timestamp and operator UTC time and authenticated user Creates an audit trail.
Decision Approve, edit, cancel, or uncertain Distinguishes refusal from a technical failure.

Implementing a takeover with Playwright

The following Node.js example keeps a headed Chromium session open. Automation fills a shipping form, then waits while an operator uses the visible window to complete MFA or correct fields. The terminal prompt is the policy gate; in production, replace it with an authenticated approval service or managed live-view interface.

Install and run

npm install playwright
npx playwright install chromium
node hitl-order.mjs

Complete example

import { chromium } from 'playwright';
import { createHash, randomUUID } from 'node:crypto';
import { createInterface } from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';

const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
const taskId = randomUUID();
const rl = createInterface({ input, output });

async function approvalGate(action) {
  const canonical = JSON.stringify(action);
  const actionHash = createHash('sha256').update(canonical).digest('hex');
  console.log('nPENDING ACTION');
  console.log(JSON.stringify({ ...action, actionHash }, null, 2));
  const answer = (await rl.question('Type approve, cancel, or edit after using the browser: ')).trim().toLowerCase();
  return { answer, actionHash };
}

try {
  await page.goto('https://shop.example/checkout', { waitUntil: 'domcontentloaded' });
  await page.getByLabel('Email').fill(process.env.CHECKOUT_EMAIL ?? '');
  await page.getByLabel('Address').fill(process.env.SHIPPING_ADDRESS ?? '');

  // Stop before MFA, CAPTCHA, payment, or final submission.
  const proposed = {
    taskId,
    type: 'submit_order',
    origin: new URL(page.url()).origin,
    item: await page.locator('[data-testid="item-name"]').innerText(),
    quantity: await page.locator('[data-testid="quantity"]').inputValue(),
    total: await page.locator('[data-testid="order-total"]').innerText(),
    instruction: 'Complete MFA if shown, verify every value, then approve only if correct.'
  };
  const decision = await approvalGate(proposed);
  if (decision.answer === 'cancel') throw new Error('Operator cancelled');
  if (decision.answer !== 'approve' && decision.answer !== 'edit') {
    throw new Error('No explicit approval');
  }

  // Re-read state after takeover; do not trust the pre-handoff snapshot.
  const current = {
    item: await page.locator('[data-testid="item-name"]').innerText(),
    quantity: await page.locator('[data-testid="quantity"]').inputValue(),
    total: await page.locator('[data-testid="order-total"]').innerText()
  };
  if (current.item !== proposed.item || current.quantity !== proposed.quantity || current.total !== proposed.total) {
    throw new Error('Checkout changed during handoff; sent to review');
  }

  await page.getByRole('button', { name: 'Place order' }).click();
  await page.getByRole('heading', { name: /order confirmed/i }).waitFor();
  console.log('Confirmed:', await page.getByRole('heading', { name: /order confirmed/i }).innerText());
} finally {
  rl.close();
  await browser.close();
}

The example deliberately does not collect a password or MFA code in the terminal. The operator enters those in the site’s visible window. If your environment uses a remote browser, disable agent input while the operator is active and make the handoff token single-use.

Use isolated sessions

Create a new browser context per task. Playwright’s isolation of cookies, storage, and related state prevents one task’s login or data from leaking into another and avoids cascading failures. Persist only the state your policy permits, and expire it when the task ends.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Designing the operator experience

Show context, not just a button

The handoff screen should show the site origin, task goal, proposed action, exact fields, and a current screenshot or live view. Mark values that came from the page as untrusted until the operator verifies them. Provide separate controls for approve, edit, cancel, and report an unexpected result.

Freeze and resume safely

When a gate opens, stop planner calls, timers, retries, and background clicks. After the operator finishes, reacquire the page and assert a user-visible result. If navigation, account, price, recipient, or confirmation text differs, invalidate the approval and request review instead of guessing.

Handle uncertain outcomes

Network failure after a payment click does not prove that no payment occurred. Mark the result unknown, query the site’s order history with read-only access, and ask a person to reconcile it. Never retry an irreversible action automatically unless the service provides an idempotency key and your policy explicitly allows the retry.

Security controls that belong in the policy

  • Least privilege: use an account limited to the task; separate purchasing, administration, and read-only roles.
  • Credential isolation: keep secrets in a vault or browser-managed login. Microsoft warns that credentials granted to browser agents can expose email, financial, social, or enterprise systems.
  • Prompt-injection resistance: treat page instructions as data. Only your policy service can create an approval request.
  • Origin and field checks: compare the current origin, account, recipient, totals, and permissions with the approved record.
  • Evidence minimization: redact personal data in logs and screenshots; set retention periods.
  • Human confirmation: Chrome guidance recommends keeping a responsible person in the loop and requesting confirmation when needed. Make confirmation mandatory for the high-impact categories in your policy.

Framework or hosted workspace?

Choose a self-managed Playwright service when you need control over deployment, network location, browser versions, and data retention. A hosted Playwright-backed workspace can shorten the work of exposing a live view and managing operator access. Evaluate both options against the same requirements:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Requirement Questions to ask
Browser coverage Does it support Chromium, Firefox, WebKit, and any branded Chrome or Edge channel you require?
Session continuity Can a person take over the exact session and return control without losing cookies or navigation?
MFA and CAPTCHA Can the operator complete them without the agent seeing the secret?
Approval granularity Can approval be tied to a specific action and field set?
Credential isolation Where are secrets stored, and what can the browser process access?
Auditability Are operator identity, timestamps, decisions, and origin recorded?
Deployment and latency Where does the browser run, and what network or data-residency constraints apply?
Observability and cost Can you inspect traces and errors, and is billing based on browsers, time, or actions?

Testing and operating the workflow

  1. Test routine navigation with deterministic fixtures and isolated accounts.
  2. Inject each checkpoint: MFA, CAPTCHA, changed totals, missing selectors, expired sessions, and a page that displays malicious instructions.
  3. Verify that no click occurs while the gate is open and that cancel leaves the system in a known state.
  4. Change a target field during handoff and confirm that the resume check blocks submission.
  5. Simulate a timeout after an irreversible click and verify the result becomes unknown rather than being retried.
  6. Review logs for secrets, excessive screenshots, and approvals that are not bound to a task and session.

Common failures and fixes

The browser closes while a person is working

Keep the browser and context owned by a long-lived worker, not a request handler. Add a lease with a timeout and show the operator when the lease expires.

The agent resumes on stale data

Re-query visible labels and values after takeover, compare them with the signed or hashed proposal, and invalidate the approval on any mismatch.

A CAPTCHA or MFA loop never completes

Stop retries, preserve the session, and return a clear “operator could not complete verification” state. Do not attempt to bypass the challenge.

Selectors break after a site redesign

Prefer accessible roles and labels, assert user-visible outcomes, and version your workflow. Treat a missing selector as a pause for review, not permission to click a nearby element.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Approval is accidentally reused

Include a random task ID, session ID, origin, action hash, and expiry in every decision. Reject approvals that do not match all of them.

Logs contain personal or payment data

Redact before storage, restrict access, encrypt retained evidence, and provide deletion and retention controls. Capture only what is needed to investigate the decision.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If you only need a clean visual record of a page or of the state reached after a human handoff, ScreenshotNeo provides a single HTTP request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for all options. This cURL request captures Stripe as WebP:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

There is a free allowance of 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is included on every plan. Sign up free to capture handoff evidence without setting up another browser worker.

Frequently Asked Questions

Should the person receive the account password during a handoff?

No. Let the person enter secrets directly in the controlled browser view, while the agent receives only a success or failure state.

What if the operator approves an action and then changes a field?

Treat the approval as invalid, create a new action proposal from the changed state, and require a fresh decision.

Can HITL automation run unattended overnight?

Only for actions your policy classifies as low risk. A scheduled job should stop and retain its session whenever a required checkpoint cannot obtain an authenticated human decision.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.