Behind the scenes

Episode 2 — The prototype that worked... on ONE form

The idea is clear on paper. The three building blocks are sketched out. All that's left is to write the code — and discover that web forms are a depth of complexity you don't suspect until you try to fill them programmatically.

The first 50 lines

The first prototype fits in about fifty lines of JavaScript. The logic is simple:

  1. Open the URL in Chromium with Playwright
  2. List all visible input, select, textarea elements
  3. For each element, fetch its name, id, placeholder, and the text of the associated label
  4. Pass this list to the LLM along with the Excel columns — it returns a mapping JSON
  5. Fill each field: page.fill(selector, value) for text, page.selectOption() for dropdowns, page.check() for checkboxes

It works. On the test form. For five minutes. Then we try a second form — a real HRIS onboarding form this time.

Disaster.

The wall of complexity

The diversity of web forms is a depth you don't appreciate until you've tried to fill them programmatically. What you imagine as "a text field" hides dozens of variants in practice:

Cascading selects. Selecting "France" in the "Country" field reveals the regions. Selecting "Île-de-France" reveals the departments. Each selection triggers an asynchronous request, a partial page reload. If you chain selectOption() calls without waiting, the second value disappears when the third one loads. You have to detect the end of the reload and wait for it.

Custom date pickers. The browser's native input[type="date"] field is often replaced by a JavaScript component with its own graphical calendar. page.fill() does nothing — the field isn't a real input. You have to simulate a click to open the calendar, click the month, click the year, click the day.

Conditionally hidden fields. Some HRIS forms only show "Bank details" fields when a specific tab is active, or when the selected contract type triggers their appearance. An inspector that lists all input elements will find these fields — but they can't be filled because they're not visible. You have to filter for elements that are actually interactive.

Disguised selects. <div> elements that behave like <select> with ARIA roles (role="combobox", role="listbox", role="option"), keyboard navigation, but no <option> tags in the DOM. page.selectOption() doesn't work on them — you have to click the div, wait for the list to open, then click the value.

Autocomplete fields. Typing "Mar" in a "City" field triggers a geocoding API request. A dropdown appears with suggestions. You have to click "Marseille" in that dropdown — not simply fill the field with the final value.

Faced with all this, form-inspector.js grows. 50 lines. 150. 300. 600. It learns to filter truly visible elements (no display:none, no visibility:hidden, no opacity:0, no pointer-events:none). It learns to deduce the label associated with a field by traversing the DOM: look for a label[for="field-id"], otherwise an aria-label, otherwise an aria-labelledby pointing to another element, otherwise the text of the nearest parent node.

AI for mapping: a non-negotiable rule

The question of mapping Excel columns to form fields is where AI enters the picture. How do you know that the column "SURNAME" should go into the last_name field? That "Birth date" corresponds to "DOB" in another form? It's semantics, natural language — exactly what an LLM is built for.

But there's a rule established from day one, non-negotiable: the LLM never sees your actual values.

It receives only two lists:

  • Column names from the Excel file: ["First name", "SURNAME", "Birth_date", "IBAN", "Job title", ...]
  • Labels of fields detected in the form: ["first_name", "last_name", "dob", "bank_iban", "job_title", ...]

It returns a JSON mapping object. That's it. The 500 rows of your Excel file — with real names, real IBANs, real dates of birth — never pass through an AI server. This is an architectural constraint, not a privacy promise buried in the terms of service.

For Excel files with complex structures — merged cells, titles in the middle of the table, data spread across multiple sheets — a first upstream LLM call is added. This call (extractExcelData) receives only representative plain text from the Excel (a few hundred characters, not sensitive values). It identifies where the header row is, which rows are section titles, which columns contain useful data. The second call (suggestMapping) then works from a clean list of headers.

First result that actually works

After a few weeks of development, the wizard takes shape: upload the Excel file, enter the form URL, validate the AI-suggested mapping, fill row by row.

First test on a real HR onboarding form: 8 out of 11 fields correctly mapped on the first attempt. The 3 misses? Columns with ambiguous names in the Excel — "Info1", "Temp_zone", "Internal_ref" — that even a human would struggle to map without context. The user corrects them with two clicks.

It's not perfect. But it's usable. It's the first time the tool does what it's supposed to do on a real case, not on a test form built for the occasion.

Next step: open it up to real people and see what breaks under actual use.

Try it with your first file

Import your Excel, paste the form URL. DataToForm pre-fills the first row for free.

Start for free

← Back to blog