When an AI agent tries to book your demo, request a quote, or sign a user up, it needs to identify each field and submit valid data. Forms with random field IDs, no labels, hidden honeypots that look like real fields, or JS-only validation all break agents. The fix is largely the same as making forms accessible to screen readers — labels, semantic IDs, and proper autocomplete. This guide covers the agent-friendly form pattern.
If a screen reader can identify and submit your form, an agent can too. Agents and screen readers both read the DOM looking for label text and ARIA — neither sees visual layout. Accessibility-equals-agent-friendly.
<!-- BAD: placeholder as label --> <input type="email" placeholder="Email" /> <!-- GOOD: programmatic label --> <label for="email">Email</label> <input id="email" name="email" type="email" autocomplete="email" /> <!-- ALSO GOOD: aria-label if visual label undesired --> <input type="search" aria-label="Search products" />
<!-- BAD: random ID, regenerated each render --> <input id="input-x7k2p9" name="firstName" /> <!-- GOOD: semantic, stable ID --> <input id="first-name" name="firstName" autocomplete="given-name" />
Single most underused agent hint:
autocomplete="given-name" → First name autocomplete="family-name" → Last name autocomplete="email" → Email autocomplete="tel" → Phone autocomplete="street-address" → Street autocomplete="postal-code" → Postcode / ZIP autocomplete="cc-number" → Credit card autocomplete="organization" → Company autocomplete="url" → Website
<!-- BAD: looks like a real field but is a trap --> <input type="text" name="email" style="display:none" /> <input type="text" name="email_real" /> <!-- Confusing for agents and screen readers --> <!-- GOOD: clearly named honeypot, hidden via CSS, labelled as such --> <div class="hp-wrap" aria-hidden="true" style="position:absolute;left:-9999px"> <label for="website-extra">Leave this blank</label> <input id="website-extra" name="url_honeypot" type="text" tabindex="-1" autocomplete="off" /> </div> <!-- Real users skip via aria-hidden + tab-index; bots fill it -->
Captcha-always blocks both spammers and agents indiscriminately. Captcha-on-suspicion is better:
// Only show captcha if behaviour suggests bot
if (submitTimeMs < 1500 || honeypotFilled || abuseScoreHigh) {
return requireCaptcha(req, res);
}
// Otherwise pass through
<!-- BAD: client-side step state, single URL --> /checkout ↓ (JS state change, URL doesn't change) /checkout (now showing step 2) <!-- GOOD: URL per step --> /checkout/details /checkout/shipping /checkout/payment /checkout/review
Agents can resume any step by URL. Refresh-safe. Bookmarkable.
<nav aria-label="Checkout progress">
<ol>
<li aria-current="step">Details</li>
<li>Shipping</li>
<li>Payment</li>
</ol>
</nav>
<!-- Connect errors to fields programmatically -->
<label for="email">Email</label>
<input id="email" name="email" type="email"
aria-invalid="true" aria-describedby="email-error" />
<div id="email-error" role="alert">
Please enter a valid email address.
</div>
<!-- Agent reads aria-describedby, knows what's wrong, fixes input -->
<!-- Render in initial HTML, not via JS -->
<form action="/api/submit" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}" />
<!-- ... fields ... -->
</form>
<!-- Agent extracts token from HTML and includes in submission -->
// If this works, agents will too
import { test } from '@playwright/test';
test('book demo form', async ({ page }) => {
await page.goto('https://example.com/demo');
await page.getByLabel('First name').fill('Test');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Company').fill('Acme');
await page.getByRole('button', { name: 'Book demo' }).click();
await page.waitForURL('**/thanks');
});