How to Fill a Multi-Step Form with an LLM Agent
7
min read
Tutorials
Short answer: most multi-step forms fail for LLM agents not because the agent can't find the fields, but because it fills them in the wrong order. Country before state. Plan before seat count. Shipping method before promo code. Every one of those has a silent ordering constraint that a flat list of "here are the inputs on this page" doesn't capture. Fix that by modeling the form as a dependency graph — which fields require which other fields to be set first — and having the agent walk the graph instead of the DOM.
That's the whole idea. The rest of this post is how to actually build it.
The problem nobody talks about
Every tutorial on "LLM agents filling web forms" covers the same ground: take a screenshot or grab the accessibility tree, ask the model which fields exist, generate a fill() call for each one. That part works fine now. Vision models and DOM extraction have both gotten good enough that finding fields is a solved problem for anything reasonably semantic.
What breaks is order.
A checkout form isn't a bag of independent inputs. It's a sequence with hidden gates:
The state/province dropdown doesn't populate until country is selected — sometimes because it's a static list swapped by JS, sometimes because it's an async call that fetches valid states for that country.
The shipping method radio group is empty or shows a spinner until an address is confirmed as deliverable.
A promo code field silently rejects input, or the "Apply" button stays disabled, until the cart has passed some minimum threshold.
A submit button is disabled — not hidden, not removed, just unclickable — until three or four upstream fields are all valid.
An agent that fills fields in DOM order, or in whatever order a vision model happens to scan the page, hits all four of these. It types a state before a country exists to constrain it. It clicks "Apply promo" while the button is still disabled and gets a silent no-op. It calls submit() and gets nothing, because the button it clicked was never actually enabled — just visually present.
None of this shows up as an error. That's what makes it worse than a broken selector. A missing element throws. A field filled out of order just quietly produces the wrong state, and the agent — or the model reasoning about the agent's actions — has no signal that anything went wrong until the final submit fails for reasons that look unrelated to the actual cause.
Why "just retry" doesn't fix it
The instinct is to wrap the whole flow in a retry loop: attempt the form, check for a success state, if it fails, re-scan the page and try again. This works for transient failures — a slow network request, a race condition on page load. It does not work for ordering problems, because the failure mode isn't "the action failed," it's "the action succeeded against the wrong version of the page."
If your agent selects a state before the country is set, that selection isn't rejected — it's just later invalidated when the country field resets the dropdown. The agent has no way to know its earlier action was silently undone unless it re-reads the entire form state after every single step, which is slow, expensive in tokens, and still fragile if the reset happens asynchronously a few hundred milliseconds after the visible DOM update.
Retrying the whole form from scratch usually "fixes" it by accident — the second pass happens to hit the fields in a workable order because the agent re-derives its plan from a page that's now further along in its own state machine. That's not a fix, it's a coin flip that gets more expensive every time it fails.
Model the form as a graph, not a list
The fix is to stop treating a form as a flat set of fields and start treating it as a small directed graph, where each field or action can declare what it depends on:
json
The requires array on each action is the whole trick. It turns "here's what's on the page" into "here's what's on the page, and here's the order you're allowed to touch it in." An agent — or the planning step of an agent, whether that's an LLM call or deterministic code — can now do a topological sort instead of guessing:
Fill anything with no
requiresfirst (country,address,promo-code).Fill anything whose dependencies are now satisfied (
state, oncecountryis set;shipping-method, onceaddressis set).Repeat until
submit-order's dependencies (state,shipping-method) are both satisfied, and only then click it.
This is a few lines of graph traversal, not a model call. That matters: ordering is a structural problem, and structural problems are exactly the kind of thing you want deterministic code handling, not an LLM re-reasoning about page layout on every step. Keep the LLM for the parts that actually need judgment — "does this label mean the same thing as that field" — and let plain code walk the dependency graph.
Where the requires data actually comes from
The honest answer is: you can't always get it for free. Some of it is inferable straight from the DOM — a <select> that's disabled until another field changes, a submit button with aria-disabled="true", a field that only renders after another one fires a change event. Static analysis of enabled/disabled states and conditional rendering catches a real chunk of this automatically.
The rest is inferable from behavior, not markup — a state dropdown that's technically enabled but returns an empty option list until a country is chosen, or a promo field that accepts input but the apply button no-ops until cart total crosses a threshold. That kind of gate doesn't show up as a disabled attribute anywhere; you only find it by watching what happens to the DOM after an action, not by reading the DOM before one.
Two more sources worth calling out explicitly, since they're easy to miss:
Async population. A dropdown that goes from 0 options to N options after a network call resolves. If your extraction takes a single snapshot, you'll miss that the field is dependent on something upstream — you have to watch it across a state change, not just read it once.
Cross-field validation that only fires on submit. Some forms don't gate the button at all — they let you click submit, then show a field-level error. That's still an ordering dependency, it's just enforced late instead of early. Worth encoding as
requiresanyway, because retrying blind after that kind of failure burns just as many tokens as retrying blind after a disabled button.
Putting it together
The pattern, independent of which extraction approach you use to build the graph:
Extract the form as a set of actions, not a flat list of inputs — capture type, field name, and any detectable
requires.Sort the actions topologically before touching the page at all.
Fill in dependency order, re-checking downstream fields after each step that's known to gate something (a
countryselect, anaddressfield) rather than after every single action.Gate the final submit on its full
requiresset actually being satisfied — not just present in the DOM, but valid.
None of this needs a bigger model or a longer prompt. It needs the page's dependency structure to be visible before the agent starts acting on it, so ordering is a lookup instead of a guess.
FAQ
Why does an LLM agent fill out multi-step forms in the wrong order? Because most extraction methods (vision, raw DOM scraping) return fields as a flat list with no indication of which fields depend on others being set first — so the agent fills them in DOM order or scan order, not dependency order.
What is a requires dependency graph in the context of form-filling agents? It's a structure where each field or action on a page declares which other fields must be completed first, turning a flat list of inputs into an ordered graph the agent can traverse deterministically instead of guessing.
Can this be solved with retries instead of an ordering model? Not reliably. Ordering failures are usually silent — the action "succeeds" against a page state that then gets invalidated — so a naive retry doesn't detect the failure, it just happens to get lucky on a later pass.
Does the LLM need to figure out field order itself? No, and it shouldn't have to. Once dependencies are captured, filling in the correct order is a topological sort — plain deterministic code, not something worth spending a model call on.
This is the exact problem Manifest is built to solve — it converts any page into a structured action manifest with a requires graph baked in, so agents get field order for free instead of reverse-engineering it from a DOM dump.