Part 3 of 7 · Form intake router series ~5 min read

How a form submission gets checked

The submission is saved and the customer already has their reply. Now the checker picks it up and decides three things: are the fields actually usable, is this a real person or spam, and what kind of request is it. The whole decision is plain Python with one small exception. No model on required fields. No model on the obvious-spam rules. A model is called only when a check is genuinely borderline — and even then, just for a second opinion, never to throw a lead away on its own.

Key takeaways

  • The checker runs right after capture, reading the saved submission — the customer is already acknowledged.
  • Spam rules live in the rules doc — honeypot field, minimum time-on-page, per-address rate limit, banned patterns.
  • Four outcomes per submission: held, accepted, needs-category, or routed-ready.
  • Anything flagged is filed in a held bucket for one-tap review — never silently deleted.
  • Bedrock fires only on borderline spam and unknown categories. Most submissions never touch a model.

The decision flow, per submission

Decision flow per submission as the checker runs A vertical decision flow diagram. At the top, an input box "Saved submission" with the form_id, the fields the customer typed, and the submission's stored metadata. Below that, a step "Check required fields" — confirm the fields the form marks as required are present and look right, such as an email that looks like an email. Below that, a check "Required fields ok?" — if a required field is missing or malformed, route to "Held" for review rather than guessing. If ok, continue. The next step "Run spam rules" — apply the deterministic rules from the rules doc: a hidden honeypot field, a minimum time-on-page, a per-address rate limit, and a banned-pattern list. The next step "Spam verdict?" — clean passes through; an obvious-spam hit routes to "Held"; a borderline score gets one cheap Bedrock Haiku 4.5 second-opinion before deciding, and on a junk verdict still routes to "Held," never deleted. The next step "Category known from fields?" — the routing table maps most forms straight to a category; if the form is a generic contact form whose category depends on the message text, call Bedrock Haiku 4.5 once to propose a category, which routes to "Needs category" only until the guess returns. When fields are valid, spam is clean, and the category is set, the submission is "Routed-ready" and handed to dispatch. Each terminal box — Held, Accepted, Needs category, Routed-ready — updates the submission's status. A note at the bottom: the rules doc holds every spam rule and category map; the checker only enforces them — and a real lead is never thrown away, only held for a human. Saved submission form_id · fields · metadata Step 1 Check required fields present · well-formed Step 2 Required fields ok? missing → held Step 3 Run spam rules honeypot · rate · patterns Step 4 Spam verdict? junk → held borderline → Haiku check Step 5 Category known from fields? unknown → Haiku guess Held review, not deleted Accepted clean, fields valid Needs category waits on guess Routed-ready hand to dispatch if missing junk clean known unknown The rules doc holds every spam rule — and a real lead is never deleted, only held for a human.
Fig 3. The checker’s decision tree, per submission. Five steps decide which of four outcomes applies. The rules doc holds every spam rule and category map; the checker only enforces them, and held leads are kept for review.

Required fields: usable beats present

The first check is the dull, important one: are the fields the form marked as required actually there and actually usable? A required email that’s blank, or that reads asdf, or a phone number that’s three digits — these aren’t leads you can act on. Rather than guess or auto-correct, the checker routes them to held with a note saying which field failed. A human glances at the held bucket, and most of the time it’s genuinely junk; occasionally it’s a real person who fat-fingered their email, and the held note tells the team exactly what to ask for.

Which fields are required, and what “well-formed” means for each, lives in the rules doc per form_id — so adding a required field to the booking form is a doc edit, not a code change.

Spam rules: cheap, layered, and biased toward keeping leads

The spam check runs as a stack of cheap deterministic rules, in order, all configured in the rules doc:

  • Honeypot. A hidden field a human never sees and never fills in, but bots happily complete. If it’s filled, the submission is almost certainly a bot — held.
  • Time-on-page. The snippet records how long the form was open. A submission completed in under a second or two is a script, not a person — held.
  • Rate limit. More than a handful of submissions from the same address in a short window is a flood — the extras are held.
  • Banned patterns. A short list in the rules doc — known spam phrases, link-stuffing, gibberish markers. A clear hit is held.

A submission that trips none of these passes as clean. A submission that clearly trips one is held. The interesting case is the in-between — a real-looking message that happens to mention a few risky words, or a slightly fast fill. For those, and only those, the checker makes one cheap Bedrock Haiku 4.5 call: “Here’s a website form submission. Is this a genuine inquiry or spam? Answer with a label and one line of reasoning.” The model’s answer is a second opinion that tips a borderline case, never the sole judge. And crucially, the worst outcome of the whole stack is held, not deleted — a wrongly-flagged real lead sits one tap away from being released, while a wrongly-passed spam costs the team ten seconds to ignore. The bias is deliberate: losing a real customer is far worse than seeing a junk one.

The category decision

Most forms answer “what kind of request is this?” by themselves. A quote form is a quote; a booking form is a booking. The routing table maps the form_id straight to a category and the checker is done. The exception is the generic catch-all contact form, where the same form might carry a sales question, a support problem, or a partnership pitch — and where it should go depends on what the customer actually wrote.

For those, the checker makes one Bedrock Haiku 4.5 call to read the message and propose a category from the short list in the rules doc (sales, support, billing, other). The submission sits at needs-category for the fraction of a second the call takes, then moves to routed-ready with the proposed category attached. If the model is unsure, it’s allowed to answer other, which routes to a general inbox a human triages — an honest “I’m not sure” beats a confident wrong guess.

Status that makes the path auditable

Every step updates the submission’s status in DynamoDB: receivedchecking → one of held, routed-ready. Each held submission carries a short held_reason (which field, which spam rule). This is what makes “what happened to that lead?” answerable: you can look up any submission and see exactly why it was held, or that it sailed through clean, with timestamps. No black box.

Why the hot path is mostly model-free

The checker could send every submission to a model and ask “is this good, and where should it go?” It doesn’t, for two reasons. First, the common case — a valid, clean, clearly-categorized submission — should be utterly predictable and instant; a model in that loop adds variance and latency for no gain. Second, model calls cost money, and the overwhelming majority of submissions are unambiguous, so the call would be wasted on most of them. Bedrock earns its place exactly twice: tipping a borderline spam call, and reading a generic contact message to guess a category. Everywhere else, plain Python reading a doc is faster, cheaper, and easier to reason about.

Next post: how a routed-ready submission finds the right tool — the routing table, the per-delivery queue, and the retries that mean a busy CRM never costs you a lead.

All posts