Back
ai-agents mcp form-api agent-native tutorial
APR 10

Agent-Native Forms - Schema is the intent, URL is the interface

Most forms break when an AI agent tries to use them. Here's how to build form endpoints that agents can discover, understand, and submit correctly — without trial-and-error or hardcoded field names.

👤
Postbox Team
· · 5 min read

Most forms were designed for humans filling inputs in a browser. An AI agent — whether it’s Claude, a custom GPT, a Cursor agent, or an autonomous workflow — isn’t a human filling inputs. It’s a program that needs to understand your form’s contract, construct a valid payload, and submit it without guessing.

The problem is that most form backends weren’t built with this in mind. An agent pointed at a typical form endpoint has no way to discover what fields are required, what format they expect, or what validation rules apply. It either hallucinates a payload and learns from 422 errors, or it fails silently.

Agent-Native Technology

Why agents fail on typical forms

When a human fills a form, they read the labels, see the field types, and understand context from the surrounding page. An agent has none of that unless you explicitly provide it.

Typical form backends offer an opaque POST endpoint. The agent knows the URL but nothing else — not what fields are expected, not what types they should be, not what validation rules apply. This forced trial-and-error approach is fragile and breaks production workflows.

The Postbox standard: self-documenting endpoints

A self-documenting endpoint responds to a GET request with its own schema. The agent discovers what to submit before it submits anything.

Every Postbox endpoint works this way. A GET with Accept: application/json returns the complete field schema — names, types, and validation rules:

GET https://usepostbox.com/api/{segment}/f/contact
Accept: application/json

The agent now knows exactly what to send. No HTML scraping. No guessing. Two HTTP calls — one to discover, one to submit:

sequenceDiagram
    autonumber
    participant A as Agent
    participant P as Postbox
    
    A->>P: GET (Accept: application/json)
    P-->>A: Returns JSON Schema (Rules, Enums, Constraints)
    Note over A: Constructs payload based on schema
    A->>P: POST (Content-Type: application/json)
    P-->>A: 201 Created

Honeypots are invisible to agents. Postbox automatically omits honeypot fields from the JSON discovery response. This ensures that an agent following the schema will never accidentally trip a spam trap intended for bots.

Giving an LLM the endpoint as a tool

If your agent is LLM-powered (OpenAI function calling, Claude tool use, etc.), you can generate the tool definition dynamically from the Postbox schema:

import httpx

async def get_form_tool_definition(endpoint_url: str) -> dict:
    """Fetch the schema and convert it to an LLM tool definition."""
    async with httpx.AsyncClient() as client:
        res = await client.get(endpoint_url, headers={"Accept": "application/json"})
        schema = res.json()

    properties = {}
    required = []

    for field in schema["fields"]:
        properties[field["name"]] = {"type": field["type"]}
        if any(r["op"] == "required" for r in field.get("rules", [])):
            required.append(field["name"])
        # Surface one_of constraints as enum for the LLM
        for rule in field.get("rules", []):
            if rule["op"] == "one_of":
                properties[field["name"]]["enum"] = rule["values"]

    return {
        "name": f"submit_{schema['slug']}",
        "description": f"Submit data to the {schema['name']} form",
        "parameters": {
            "type": "object",
            "properties": properties,
            "required": required,
        },
    }

Now the LLM receives a tool definition generated from the live schema. When you update a form in Postbox, the agent re-discovers the contract and updates its own tool parameters automatically. No code changes required.

The Postbox MCP Server

For users of the Claude desktop app or Cursor, the Postbox MCP server gives your agent direct access to your infrastructure:

{
  "mcpServers": {
    "postbox": {
      "type": "http",
      "url": "https://usepostbox.com/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_API_KEY"
      }
    }
  }
}

With MCP connected, your agent can:

  • List forms and their live schemas.
  • Submit data on your behalf.
  • Search submissions and review smart replies.

Building a custom agent that submits forms

If you’re building a custom autonomous script—a Python workflow or a multi-step task—the integration is two clean HTTP calls. You don’t need to hardcode field names or validation logic into your script; you let the agent discover them at runtime.

import httpx

async def submit_to_postbox(endpoint_url: str, data: dict) -> dict:
    async with httpx.AsyncClient() as client:
        # Step 1: Discover the contract
        schema_res = await client.get(endpoint_url, headers={"Accept": "application/json"})
        schema = schema_res.json()

        # Step 2: Validate data against schema (Optional)
        # You can check for required fields or regex patterns before the round-trip
        
        # Step 3: Submit
        res = await client.post(endpoint_url, json=data, headers={"Content-Type": "application/json"})
        return {"success": res.status_code == 201, "status": res.status_code}

This pattern makes your agent future-proof. If you add a “Department” field to your form in the Postbox dashboard, your agent will immediately see it in the GET response and can adapt its payload without a single line of code change.

Private forms and agent authentication

For internal tools, backend-to-backend integrations, or sensitive data collection, you can make a form Private. Private forms require a Bearer submission token on every POST.

The schema discovery endpoint (GET) for private forms includes an authentication block explaining the requirement. This allows a sophisticated agent to learn not just what to submit, but how to authorize its request.

# Discovery response for a private form
{
  "visibility": "private",
  "authentication": {
    "type": "bearer",
    "location": "header",
    "name": "Authorization"
  },
  "fields": [...]
}

What happens when the schema evolves?

When you update a form’s schema in Postbox, we generate a new endpoint URL. This is a critical design choice: it prevents breaking changes.

  • Legacy Agents: Older scripts using the previous URL continue to work against the old schema.
  • New Agents: You point new integrations at the new URL, where they discover the updated requirements.

This “Schema Versioning” approach means you can iterate on your data collection infrastructure without worrying about which agents are currently “in the field” using your endpoints.

The contract is the interface

The same Postbox endpoint works for a human filling a form in a browser, a curl script in a CI pipeline, and an autonomous agent acting on a user’s behalf. The contract — the schema, the validation rules, the error format — is identical for all three.

When you build your forms as contracts rather than UIs, you get agent compatibility for free.


Ready to build for the agentic web? Try Postbox free — every endpoint is self-documenting and agent-ready out of the box. No credit card required.

Have thoughts?
Or connect for more dispatches.