FerryBrowser Agent
What it does for you
A browser agent that validates CSV rows, enters them into a web form one at a time, recovers from session timeouts, and reconciles every confirmation number, proven end to end against a fictional practice portal.

Screenshots

In more detail
Ferry takes a spreadsheet of orders, checks every row, types the good ones into a web form one at a time, and hands back a report. Every row ends up with a confirmation number or a plain reason why it has none. Small teams still do this re-keying by hand, and the mistakes hide: a row that silently failed, or one entered twice after a timeout.
Honest limits
Everything ran against a fictional practice portal with a pinned practice clock, not a real site. The end-to-end run on record is the Playwright stand-in. The case study does not record Claude in Chrome itself executing the runbook, and there is no Claude API mode. Portal rules live in the core, so pointing Ferry at a second form would take code changes.
How I built it
This was one of the Claude Cowork projects. I told Cowork to one-shot all of them, and I picked the task shape: CSV rows into a portal form, confirmation numbers back, a Claude in Chrome runbook, and a Playwright stand-in with real output. Cowork built, tested, screenshotted, and documented it in one autonomous pass.
The target is a made-up "Ferry Practice Portal," so nothing imitates a real organization. It has real friction: a late session token, a dropdown that loads from the server, a calendar date picker, and a "Your session expired" page on about 1 in 8 submissions.
Cowork chose the safety rules: invalid rows never enter the plan, one submit click per attempt, classify every result page before acting, and pause on anything unexpected. Then it ran the Playwright stand-in against the live practice portal twice. The real run is in samples/: 16 rows, 5 never submitted because they failed validation, 10 confirmed (3 after a session-expired retry), 1 rejected for a backordered item. The second run, without a reset, returned all 10 original numbers as already on file. Nothing was entered twice.
What I learned
- The task plan should be data. Steps, selectors, expected outcomes, and an outcome-to-action policy all live in the core, so the Claude in Chrome runbook, the Playwright stand-in, and the in-browser simulator all run the same script.
- A session-expired page and a duplicate page look alike but mean opposite things. Expired means not saved, so redo it. Duplicate means saved, so record the existing number. Having the portal return the original number on a resubmit is what makes a crashed run safe to restart.
- Classify a result page by two signals, a data marker and the visible heading, and call it unknown when they disagree. That turns a surprise layout into a pause instead of a wrong record.
- A check character on confirmation numbers is cheap insurance. A one-character misread becomes a flagged row, not a bad number in the report.
- When samples are real output, pin them in the tests. After a validation change,
verify.mjscaught a stale sample report, so it was regenerated from a new real run.
What carried forward
The core, ferry.js, is one DOM-free module shared by the console, the portal, the server, a CLI, and the tests. That is the June pattern (one tested module, many callers) pushed further. "Plan as data, runners stay dumb" is what I would reuse for any browser automation.
If I took it further
I would record a short trace per attempt, with a screenshot on any pause, so a paused row can be reviewed without rerunning it, and load the portal rules from a JSON file so Ferry could target a second form.
#073 Built with Claude Cowork September 23, 2026 225 automated checks