PageAgent vs. Playwright — Feasibility (Technical)¶
Status: research spike, no code changed. This document evaluates whether
Alibaba PageAgent (npm: page-agent, docs:
alibaba.github.io/page-agent) could replace or
supplement Microsoft.Playwright in WorkWingman's automation layer
(src/WorkWingman.Infrastructure/Automation/*). It answers five questions: Claude support,
integration architecture, the "one prompt replaces all selectors" thesis, cost/latency/safety
tradeoffs, and a recommendation.
Plain-language version: ../plain/pageagent-feasibility.md
0. What PageAgent actually is¶
PageAgent (22.6k★, MIT, TypeScript monorepo, latest v1.11.0) is "the GUI Agent living in
your webpage" — an in-page JS agent that takes a natural-language instruction
(agent.execute('Click the login button')) and drives the DOM itself, via an LLM-in-the-loop
agentic step cycle. It is explicitly client-side web enhancement, not server-side browser
automation — there is no headless-browser server, no Node/Python driver process. It runs
inside a page the user already has open, either via <script>/npm import or, for
cross-page/multi-tab tasks, an optional Chrome extension.
Monorepo layout (packages/):
| Package | Role |
|---|---|
@page-agent/core |
Headless agent loop (PageAgentCore), tools, prompts — no UI |
@page-agent/page-controller |
DOM reading/writing: builds a "FlatDomTree", indexes interactive elements, executes actions |
@page-agent/llms |
LLM client abstraction + the one shipped provider, OpenAIClient |
@page-agent/page-agent |
Public page-agent npm package — adds a UI panel on top of core |
@page-agent/ui |
Panel widget + i18n |
@page-agent/extension |
Chrome extension (WXT + React) for multi-tab/page tasks |
@page-agent/mcp |
MCP server — lets an external AI client (Claude Desktop, Copilot) drive a browser through the PageAgent extension |
@page-agent/website |
Docs/landing site |
DOM pipeline: Live DOM → FlatDomTree (page-controller) → simplified text description
of interactive elements, each given a stable numeric index → sent to the LLM as part of the
prompt → LLM picks an index and an action → page-controller executes it (click/type/etc. by
index) → tree rebuilt → loop continues. No screenshots, no vision model, no coordinates — this
is the "text-based DOM manipulation" the task description calls out, and it's confirmed at the
source level (PageAgentCore.ts, page-controller).
1. Claude / Anthropic support¶
Short answer: not out of the box, but pluggable — with real friction.
What ships today¶
@page-agent/llms exports exactly one concrete provider client: OpenAIClient. There is no
AnthropicClient, ClaudeClient, Bedrock, or Vertex client anywhere in the monorepo. The
developer guide's only documented configuration path is three env vars —
LLM_MODEL_NAME / LLM_API_KEY / LLM_BASE_URL — pointed at an OpenAI-compatible
/chat/completions endpoint (the shown examples are Alibaba's DashScope/Qwen and a local
Ollama). "Bring your own LLM" in this project's docs means "bring your own OpenAI-compatible
endpoint," not "bring your own provider SDK."
The interface a provider must satisfy¶
// packages/llms/src/types.ts (paraphrased)
interface LLMClient {
invoke(
messages: Message[],
tools: Record<string, Tool>,
abortSignal?: AbortSignal,
options?: InvokeOptions,
): Promise<InvokeResult>
}
interface Message {
role: 'system' | 'user' | 'assistant' | 'tool'
content?: string | null
tool_calls?: { id: string; type: 'function'; function: { name: string; arguments: string } }[]
tool_call_id?: string
name?: string
}
interface Tool<TParams = any, TResult = any> {
description?: string
inputSchema: z.ZodType<TParams> // Zod schema, converted to JSON Schema per-provider
execute: (args: TParams) => Promise<TResult>
}
This is a single-tool-call-per-step, forced tool-use loop: PageAgentCore packs every
available action into one big "MacroTool" (evaluation_previous_goal, memory, next_goal,
action: { [toolName]: toolInput }) and calls invoke() once per agent step, expecting exactly
one tool call back. The OpenAIClient reference implementation sends:
POST {baseURL}/chat/completions
Authorization: Bearer {apiKey}
{
model, messages, tools,
parallel_tool_calls: false,
tool_choice: "required" // or {type:'function', function:{name}} to force a specific tool
}
choice.message.tool_calls[0].function back out. No streaming is used — it's one
blocking call per step.
Can Claude satisfy this contract?¶
Yes, structurally — Claude's Messages API supports forced tool use
(tool_choice: {type: "any"} to force some tool call, or {type: "tool", name: "..."} to
force a specific one) and returns tool_use content blocks with a name + JSON input, which is
semantically equivalent to OpenAI's tool_calls[].function. Claude does not speak the
OpenAI wire format directly, though — different endpoint (/v1/messages vs
/v1/chat/completions), different envelope (content: [{type:'tool_use', ...}] vs
tool_calls: [{function:{...}}]), different single-system-message convention, and a different
error/rate-limit response shape than errors.ts expects (NETWORK_ERROR, RATE_LIMIT,
SERVER_ERROR, AUTH_ERROR, CONTEXT_LENGTH, CONTENT_FILTER, plus PageAgent-specific
NO_TOOL_CALL / INVALID_TOOL_ARGS / INVALID_RESPONSE / INVALID_SCHEMA).
Two concrete integration paths:
Path A — OpenAI-compatible proxy in front of Claude (fastest, zero PageAgent code changes).
Point LLM_BASE_URL at any proxy that translates OpenAI chat-completions calls to Anthropic
Messages calls (e.g. a small local Express/Node shim, or a hosted compatibility layer). This
is the path the task hint already anticipates ("an OpenAI-compatible proxy") and it needs zero
changes inside PageAgent — OpenAIClient just talks to a different baseURL. Effort: build
and host a thin translation shim (request/response reshaping + SSE-off since PageAgent doesn't
stream); this is genuinely a few hundred lines, but it's new infrastructure WorkWingman would
own and maintain, not something PageAgent gives you.
Path B — write a real AnthropicClient : LLMClient (clean, more work).
Implement invoke() against https://api.anthropic.com/v1/messages directly:
- Map messages[] → Anthropic's system (top-level string, pulled out of the array) + messages[] with user/assistant roles only (Anthropic has no system or tool role in the array — tool results become user messages containing a tool_result content block keyed by tool_use_id).
- Convert each Tool.inputSchema (Zod) → Anthropic's tool JSON Schema shape ({name, description, input_schema}}) instead of OpenAI's {type:'function', function:{...}} wrapper — the existing zodToOpenAITool() helper is not reusable as-is; a zodToAnthropicTool() sibling is needed (small, since both are JSON Schema underneath).
- Set tool_choice: {type: "any"} (equivalent of tool_choice: "required") or {type: "tool", name} for forced single-tool steps.
- Parse response.content for the tool_use block (name, input — already an object, not a JSON string, so the JSON.parse() step OpenAIClient does is skipped) instead of choice.message.tool_calls[0].
- Map Anthropic's stop_reason / HTTP errors onto PageAgent's InvokeErrorTypes (overloaded_error → SERVER_ERROR, invalid_request_error on auth → AUTH_ERROR, max_tokens/context overflow → CONTEXT_LENGTH, etc.).
- Map usage.input_tokens/output_tokens/cache fields → PageAgent's usage shape (prompt/completion/cached/reasoning tokens).
This is a genuinely small, well-scoped adapter — maybe 150–250 lines plus tests — because the
LLMClient interface is narrow and PageAgent's own OpenAIClient is the template. It's the
kind of "one obvious replacement point" pattern this repo already uses for other stubs
(UnconfiguredGoogleTokenProvider, TemplateDrafter : IClaudeDrafter). Nothing in
PageAgent's core loop, tool set, or prompts is OpenAI-specific — the coupling is isolated to
one package (@page-agent/llms).
Verdict: Claude can be wired in, but WorkWingman (or a community contributor) has to write the adapter — PageAgent does not ship one, and no GitHub issue/PR reviewed here indicates one is planned. Budget it as a small, contained TypeScript task, not a blocker, but not "free" either.
2. Integration architecture — where would this actually live?¶
WorkWingman's automation today is entirely server-side C#:
flowchart LR
Angular["Angular renderer<br/>(Live Run viewer — placeholder today)"]
API[".NET API<br/>controllers"]
Engine["WorkdayAutomationEngine<br/>(C#)"]
Session["BrowserSession<br/>Microsoft.Playwright<br/>persistent Chromium profile"]
ATS["Workday tenant<br/>(real browser, visible)"]
Angular -->|HTTP loopback| API --> Engine --> Session --> ATS
Angular -.polls RunStatus.-> API
BrowserSession (src/WorkWingman.Infrastructure/Automation/BrowserSession.cs) launches a
headed, persistent Chromium context via Microsoft.Playwright — this is deliberate: the
"Live Run viewer" is meant to mirror a real, visible browser window so the user can watch and
intervene. WorkdayAutomationEngine drives that page with Playwright.FillAsync/ClickAsync
using selector strings (data-automation-id*='email'), logs every fill with a confidence score,
and raises a JudgementCall that pauses the run when confidence is below threshold.
PageAgent is architecturally the opposite shape: it must run inside a JS execution context
that has direct DOM access to the target page — i.e., inside the page's own window, or in a
content-script-like context with access to that document. It cannot run inside .NET, and it
cannot be imported into WorkdayAutomationEngine.cs — there is no cross-language embedding
story (no Node-in-.NET here; ClearScript/Jint could run some JS but not a DOM-manipulating
agent with no DOM to manipulate).
Where it would have to live, concretely¶
Two realistic placements, both requiring new plumbing that doesn't exist today:
Option 1 — content script inside an Electron WebContentsView (matches the existing roadmap stub).
architecture.md already documents "Live-run browser — the Angular viewer shows a placeholder;
in Electron, attach a WebContentsView and drive it from WorkdayAutomationEngine" as a
deliberate stub. If that WebContentsView is adopted instead of (or alongside) Playwright's
Chromium:
- The Electron main process creates a WebContentsView pointed at the Workday apply URL (Electron owns this window instead of Playwright's persistent-context Chromium — losing Playwright's persistent-profile/anti-automation flags unless re-implemented via Electron session partitions and Session.webRequest).
- PageAgent (page-agent npm package) is injected into that view's renderer via webContents.executeJavaScript(...) or a Chrome DevTools Protocol (Target.exposeDevToolsProtocol)-style preload script — effectively the same trick Electron already uses for its own preload.js.
- The .NET backend cannot call PageAgent's JS API directly — it has to go through Electron. That means a new IPC leg: .NET API → (new) local control channel → Electron main → WebContentsView.webContents.executeJavaScript('agent.execute("...")') and the reverse for results/events (onBeforeStep/onAfterStep/onAskUser callbacks would need to be forwarded back over that same channel as SSE/WebSocket/polling, since the .NET API and Electron main are separate processes today — the app already bridges Angular↔Electron via electron/preload.js's contextBridge, but there is no existing API↔Electron-main channel; today Electron only spawns the API as a child process and loads the Angular build).
- Concretely new pieces needed: (a) a local RPC channel .NET↔Electron (named pipe, loopback WebSocket, or a small HTTP callback server in Electron main), (b) a PageAgent injection + result-relay layer in electron/main.js, (c) a rewrite of WorkdayAutomationEngine's public surface to speak "task strings in, step events out" instead of "selector strings in, IPage out."
Option 2 — PageAgent's own MCP server as the bridge (@page-agent/mcp).
@page-agent/mcp already exists to let an external AI client (its docs name Claude Desktop and
Copilot) drive a browser through the PageAgent Chrome extension over a local WebSocket hub
(default port 38401). In theory the .NET API could itself be an MCP client, calling
execute_task on that server instead of building custom IPC. But this pulls in the Chrome
extension (a third install/permission surface beyond the app itself) and assumes the user's
regular Chrome — not Electron's embedded Chromium — is the automation target, which conflicts
with the existing "invisible unless watching the Live Run view" model and the persistent-profile
design in BrowserSession. This path trades custom IPC for a heavier dependency (extension +
its own WebSocket hub) and is a worse fit for an embedded, single-purpose automation view.
What would still be Playwright (if anything)¶
Playwright's actual value in BrowserSession isn't "clicking things" — it's:
- Persistent authenticated profile management (LaunchPersistentContextAsync against a fixed profile dir so LinkedIn/Workday logins survive across runs);
- Anti-detection launch flags (--disable-blink-features=AutomationControlled, real UA, SlowMo);
- Cookie extraction for non-browser HTTP calls (GetCookieHeaderAsync, handed to Flurl clients);
- The only Playwright.NET E2E harness (tests/WorkWingman.E2E) that drives the built Angular app itself (not the ATS) against a stubbed API via route interception — this is unrelated to ATS automation and has no PageAgent equivalent at all; it would not change regardless of what happens to WorkdayAutomationEngine.
If PageAgent replaced the in-page action-taking layer, Playwright (or Electron's own
WebContentsView/session APIs) would likely still own outer harness concerns: launching the
browsing surface, session/profile persistence, cookie/header extraction for the Flurl-based
scraping paths (LinkedInJobsScraper largely doesn't need an LLM at all — it's bulk link
harvesting + fallback-selector text extraction, which is a batch/read-only task PageAgent isn't
designed for and an LLM call per field would be wasteful for). tests/electron-e2e (the
Playwright-for-Electron smoke test) is also unaffected — it tests that the shell boots and
reaches the loopback API, nothing about ATS automation.
3. The big thesis — does natural language replace per-ATS selector engineering?¶
The premise, calibrated to what's actually in this codebase today: WorkWingman currently
hand-codes one fallback-selector case in production automation code —
WorkdayAutomationEngine.SignInAsync's
input[type='text'][data-automation-id*='email'], input[type='email'] — plus a broader,
well-documented fallback-chain pattern in LinkedInJobsScraper.FirstTextAsync (title/
company/location/description each tried against 2–4 selector variants because "LinkedIn markup
shifts often"). AtsDetector only classifies Greenhouse/Lever/iCIMS/UKG from URL/HTML
markers — it does not yet drive any of them. The generalized claim in the research prompt (ADP
uses formcontrolname, iCIMS embeds forms in iframes, etc.) is a plausible industry-general
pattern — each ATS vendor does have its own DOM conventions — but it is not yet something this
repo's code demonstrates first-hand beyond Workday + LinkedIn. Treat the thesis as "would this
generalize to ATSes WorkWingman hasn't automated yet," not "this replaces code that's already
been painfully hand-tuned across five ATS vendors here."
Is "one natural-language prompt per field, across ATS vendors" plausible? Partially, with real caveats:
- Plausible upside: PageAgent's DOM-to-text pipeline (
FlatDomTree→ indexed interactive elements) is vendor-agnostic by construction — it doesn't know or care whether an input hasdata-automation-id,formcontrolname, or no attribute at all; it just needs the element to be in the accessibility/DOM tree with enough surrounding text (label, placeholder, nearby heading) for the LLM to identify it as "the Education section." That's exactly the generalization Workday'sdata-automation-id*='email'and ADP'sformcontrolnameselectors can't give you — a selector chain must be written and maintained per vendor; a natural-language instruction in principle does not. - iframes are the sharpest real risk. iCIMS (and many ATS embeds) put the actual application form inside an
<iframe>. Playwright has first-classframe_locatorsupport for this. PageAgent's DOM pipeline, as documented, walks the live DOM tree it has access to — same-origin iframes are generally reachable viacontentDocument, but cross-origin iframes are opaque to any in-page script for the same reason they're opaque to any other page-embedded JS (browser same-origin policy) — page-agent's own docs/tools list shows no explicit "switch into iframe N" tool, unlike Playwright's explicitFrameLocatorAPI. This is a structural limitation of "runs as page JS," not a bug to be fixed — an in-page agent is bound by the same cross-origin sandboxing as any other script on that page. Multi-page ATS flows crossing origins (SSO redirects, embedded payment/background-check widgets) hit the same wall. - No selector chain still means no selector chain to maintain — but it doesn't mean zero per-vendor work. Even "one prompt" needs page-specific
instructions/customSystemPromptcontext to work well in practice (PageAgent's config exposes exactly this:getPageInstructions(),customSystemPrompt). In effect, hand-written selector fallback chains would be replaced by hand-written prompt/context tuning per ATS quirk — real engineering effort moves from "guess the CSS" to "guess the wording and the guardrails," which is a different and probably smaller-surface problem, but not a zero-effort one. - Determinism goes down exactly where selectors made it predictable. A selector chain either matches or doesn't — it's data, testable offline. A natural-language step asks an LLM to look at a DOM snapshot and choose an index; the same page can, in principle, get a different action on a different run (see §4).
- Confidence-gated judgement calls become harder to reproduce.
WorkdayAutomationEngine.TryFillFieldAsyncalready has an explicit, deterministic confidence/threshold check before trusting a fill. PageAgent's loop bakes reflection into the model's ownevaluation_previous_goal/memoryfree text — there's no numeric confidence value surfaced by the framework the way WorkWingman'sRankedOption/JudgementCallmodel expects. Bridging "LLM free-text self-assessment" into "numeric confidence vs. threshold, ranked alternatives" is new glue code either way.
Verdict on the thesis: plausible as a direction — the core insight (bypass DOM-shape-specific selectors, key off visible text/labels/role) is sound and is exactly why frameworks like this and browser-use (which PageAgent explicitly derives components from) exist. But "replace ALL per-ATS selector engineering with ONE prompt" overstates it: iframes, cross-origin flows, and the loss of a first-class confidence/threshold model mean the realistic outcome is fewer hand-tuned selectors, not zero hand-tuned anything, and a shift of engineering effort from CSS chains to prompt/guardrail chains.
4. Tradeoffs¶
Cost — LLM calls per application¶
PageAgent calls the LLM once per agent step (one invoke() per iteration of the execute()
loop; maxSteps defaults to 40). A single Workday application, mapped onto
WorkdayAutomationEngine.StepLabels, plausibly decomposes into many agent-steps, not one call
per page section — because each field-level action (click a text input, type into it, open a
dropdown, select an option) is its own step in PageAgent's model, not something batched into one
call per section. Rough estimate for one Workday application:
| Section | Fields/actions | Est. agent steps (≈1 LLM call each) |
|---|---|---|
| Sign in | email, password, submit | 3–4 |
| Contact info | name, address, phone, email confirm | 6–10 |
| Work history | 2–3 jobs × (title, employer, dates, description) | 15–30 |
| Education | school, degree, field, dates | 6–10 |
| Skills & languages | multi-select / tag inputs | 4–8 |
| Portfolio links | GitHub + gitfront URLs | 2–4 |
| Demographics (EEO) | 4–6 dropdowns | 4–8 |
| Review & submit (pause, no auto-submit) | scroll + verify, no submit call | 2–4 |
| Total | ~45–80 LLM calls per application |
That is a lot more than Playwright's zero LLM calls for the equivalent flow today (Playwright
selectors are free at runtime; the only "cost" is the one-time engineering effort to write them).
At typical hosted-model pricing this is real but not huge per application (tens of cents to
roughly a dollar depending on model tier and prompt-context size — PageAgent resends a running
<agent_history> block each step, so cost grows worse than linearly with step count unless the
implementation prunes history). At WorkWingman's actual scale (one user, some number of
applications per week) this is affordable; it stops being affordable/attractive the moment
"every field is an LLM call" meets a job-search user applying to dozens of jobs a day, or a future
multi-tenant hosted version.
Latency¶
Each step is a full round trip to a hosted LLM (or a local Ollama model, per PageAgent's own
example — which trades API latency for GPU-bound local inference latency). 45–80 sequential
steps (the loop is inherently sequential — each step's action depends on the DOM state produced
by the last) at even 1–3 seconds per call is 1–4 minutes of LLM think-time alone per
application, before accounting for page load/render waits already in the loop (wait tool,
stepDelay). Playwright's equivalent flow runs in seconds once selectors are correct — the
200-800ms SlowMo/PageDelay pacing WorkWingman uses today is deliberately-added human-like
delay, not a floor imposed by the automation method.
Determinism / reproducibility¶
This is the sharpest tradeoff. WorkWingman's testing philosophy is explicit: "Deterministic,
always... Flakiness is treated as a defect in the test, not a nuisance to paper over"
(docs/technical/testing.md). Playwright selectors are exactly that: the same DOM produces the
same fill, every time, and that's provable/testable without a live network call. An LLM-in-the-
loop agent is fundamentally not deterministic in that sense — even at low temperature, model
updates, provider-side routing, and subtly different DOM snapshots (ads, A/B tests, loading
races) can change which index gets clicked. This directly conflicts with the repo's
"determinism/churn" CI job, which re-runs suites ×5 and fails on any flip — an LLM-driven
automation path is close to un-unit-testable in that framework without heavy mocking of the LLM
client itself (which then tests the harness, not the actual field-selection behavior).
The never-auto-submit safety contract¶
WorkWingman's hard rule — "the final submit step ALWAYS waits for the user (locked behavior)"
(WorkdayAutomationEngine class doc) and "It never hits 'submit' without you"
(docs/plain/architecture.md) — is currently enforced in code: there is simply no call path
that reaches a submit click without a human-triggered resolve on a paused run. PageAgent has
no equivalent framework-level guard. Its tool set (click_element_by_index, input_text,
select_dropdown_option, scroll*, execute_javascript, wait, ask_user, done) treats a
submit button as just another clickable index — nothing in PageAgentCore's hooks
(onBeforeStep/onAfterStep/onBeforeTask/onAfterTask) is action-aware enough to
intercept "the LLM is about to click the button labeled Submit" without WorkWingman writing that
interception itself (e.g., a customTools override that removes/wraps click_element_by_index
to inspect the target element's text/role before allowing a click near known submit-like labels,
or restricting the tool it forces on the "review" step to ask_user/done only). This is
buildable — PageAgent's onAskUser and customTools extension points are exactly the seam
you'd use — but it is not a safety net PageAgent gives you for free; today's guarantee comes
from WorkdayAutomationEngine simply never calling a submit selector, and any PageAgent
integration has to re-earn that guarantee explicitly, per ATS, forever, since the LLM chooses
actions dynamically rather than following a fixed code path.
Offline / local-fixture testability¶
Playwright's route-interception pattern already used in tests/WorkWingman.E2E
(StaticSiteServer + StubbedApi, CORS-hardened, driving the built Angular app against fully
local fixtures) has no analogue for PageAgent without also mocking the LLM client. You could feed
PageAgent a local static HTML fixture of a Workday page (page-controller doesn't care where the
DOM comes from), but every step still calls a real or mocked LLM — testing "does PageAgent fill
this fixture correctly" means either (a) burning real LLM calls in CI (slow, costs money,
non-deterministic — directly against the churn-test philosophy), or (b) mocking LLMClient.
invoke() to return canned tool calls, which tests the harness plumbing but not the thing you
actually care about (does the model correctly interpret this DOM). Playwright selector tests
have no such dilemma — the assertion is "did this selector find and fill the right element."
Failure modes¶
| Failure mode | Playwright (today) | PageAgent |
|---|---|---|
| Selector doesn't match (markup change) | Silent skip / fallback chain exhausted → empty field, logged | LLM picks some index — could silently fill the wrong field with a plausible-looking but incorrect value |
| Ambiguous field | N/A — selectors are exact or absent | LLM may guess confidently and wrongly (hallucinated confidence) rather than surfacing a JudgementCall |
| Rate limit / auth / outage on the model provider | N/A | Entire run stalls or errors (NO_TOOL_CALL, RATE_LIMIT, AUTH_ERROR from errors.ts) — a new external dependency in the critical path of every field, not just document drafting |
| Cross-origin iframe content | Explicit FrameLocator API handles it |
Opaque/inaccessible to in-page JS (see §3) |
| Model does something destructive | Not possible — code path doesn't include destructive selectors | Possible unless explicitly guarded (see above) |
5. Recommendation¶
Not a wholesale replacement. Not "not yet" either — a scoped, opt-in hybrid, and only after the Playwright-based Workday path actually exists end-to-end.
Today WorkdayAutomationEngine's apply-automation is itself only partially wired (per
connections-and-sso.md: "Seeded metadata... apply automation not wired"), and the Live Run
viewer is a documented placeholder. There is no working Playwright ATS-fill baseline yet to
"replace." That changes the framing: this isn't yet a rip-and-replace decision, it's a build
decision — and PageAgent is a legitimate one of two build paths, not a strict upgrade over
the other.
Where PageAgent's strengths land¶
- Novel/unautomated ATS vendors the app hasn't hand-coded yet (Greenhouse, Lever, UKG,
future ones
AtsDetectoralready classifies but doesn't drive) — where the alternative is writing a new selector chain from scratch anyway. Here PageAgent's marginal cost (an LLM call per field) competes directly against the marginal cost of engineering time to hand-roll selectors for a vendor used rarely. That's a genuinely favorable trade for a long-tail vendor. - A fallback path when a known-good selector chain breaks — Workday changes markup; instead
of the run failing outright, drop into a PageAgent-driven attempt for that one field/section,
flagged clearly in the run log as "AI-assisted fallback" (lower trust, always routes to a
JudgementCall-equivalent confirmation regardless of the model's stated confidence).
Where Playwright should stay in charge¶
- Workday (the one ATS with committed selectors already) and any other vendor once its selector chain is written and tests exist — deterministic, fast, free, already fits the CI determinism-churn model.
- The never-auto-submit guarantee — keep this enforced by the harness, not by trusting an LLM (or a PageAgent guard) to reliably self-censor. Concretely: whatever drives the final review/submit step should never route to a tool capable of a submit-labeled click, regardless of which engine filled the preceding fields.
- LinkedIn scraping (
LinkedInJobsScraper) — bulk, read-only, no judgement calls; an LLM call per link would be pure waste against a pattern that already works. - The outer harness — profile persistence, anti-detection flags, cookie handoff to Flurl,
and both existing E2E suites (
tests/WorkWingman.E2E,tests/electron-e2e) are Playwright- shaped problems unrelated to in-page field-filling and are unaffected either way.
Concrete phased plan¶
- Phase 0 (prerequisite, not PageAgent-related): finish the Playwright Workday path.
Wire
WorkdayAutomationEngineend-to-end against a real tenant behind a feature flag, with theWebContentsViewLive Run viewer from the architecture roadmap. This is necessary regardless of PageAgent's fate — right now there's no working baseline to compare against or fall back to. - Phase 1 — spike the Anthropic adapter in isolation. Write and unit-test an
AnthropicClient : LLMClient(or a thin OpenAI-compatible proxy, whichever is faster to ship) against@page-agent/llms's existing test patterns (OpenAIClient.test.tsas the template), with no WorkWingman integration yet. This de-risks "can Claude drive PageAgent at all" cheaply and separately from the harder Electron/IPC question. - Phase 2 — prototype the Electron bridge on one throwaway page. Build the minimal
.NET ↔ Electron main ↔ WebContentsViewcontrol channel against a single local HTML fixture (not a live ATS), provingexecute("fill the email field")round-trips step events back to the .NET side. Measure real steps-per-field and $/application on that fixture before touching a live tenant. - Phase 3 — hybrid fallback, one ATS vendor, feature-flagged and off by default. Wire
PageAgent as the fallback path only when a Playwright selector chain is absent or has
exhausted its fallbacks for a given field, on a vendor Playwright doesn't yet support
(Greenhouse or Lever, since
AtsDetectoralready classifies them). Every PageAgent-filled field routes through the existingJudgementCallpause regardless of the model's self- reported confidence, until real-world accuracy data justifies loosening that. - Phase 4 — decide, with data. After N real applications through the hybrid path, compare fill accuracy, $/application, time/application, and judgement-call rate against the Playwright baseline. Only expand PageAgent's footprint (more vendors, higher auto-trust) if the data clears a bar WorkWingman sets in advance — don't let "it worked in the demo" be the bar.
Bottom line: treat PageAgent as a fallback/long-tail-vendor tool bolted onto a Playwright-first architecture, not a Playwright replacement. The natural-language DOM abstraction is real and valuable specifically where hand-written selectors don't exist yet or keep breaking; it is not a safe or cheap substitute for the deterministic, free, already-tested path WorkWingman has for Workday, and it does not come with the never-auto-submit guarantee for free — that has to be re-built and re-verified per integration.
Related docs¶
- Plain-language: ../plain/pageagent-feasibility.md
- architecture.md · connections-and-sso.md · testing.md · security.md
- Doc index: ../README.md
- PageAgent: github.com/alibaba/page-agent · docs
- Anthropic tool use / forced tool choice: docs.anthropic.com