What you will take away from this lesson
ACME’s vendor team receives supplier applications by email: a PDF form, a certificate of insurance attached, a tax ID typed into the body. Somebody copies each one into a spreadsheet. The team asked the agent for a form instead.
The prompt was one paragraph: company and contact, the category they would supply and the store they would deliver to first, expected annual volume, earliest delivery date, tax ID kept private, a certificate of insurance, and agreement to the supplier code of conduct. This lesson reads what the agent built from it.
Learning Objectives
- 01Ask the agent for a form and read what it writes: a spec and a submit handler.
- 02Choose field types and limits, and fill choices from live data.
- 03Refuse a submission field by field from the submit handler.
- 04List the checks the platform makes before the form’s automation sees a submission.
- 05Find the hosted page and the table every accepted submission lands in.
- 06State what a form does not do.
Where this lesson sits
Lessons 701 and 702 built endpoints that systems call. A form is the same kind of endpoint, answered to people instead.
700 Series: Endpoints and Forms
- 701An automation that answers a requestWhat an endpoint is, the request a handler is handed and the answer it returns, who binds it, the limits a request runs under, and what a caller is told.
- 702A lookup a partner system callsThe three access modes, API keys and per-caller limits, one audit row per request, refusals by reason, signed tokens, and the Endpoints page.
- 703Your first formA form the agent writes: fields, live choices, field-by-field refusals, the checks the platform runs first, the hosted page, and submissions as a table.
- 704Putting the form on your site, or in a reportEmbedding in an asset and on an allowed site, named bot checks, the honeypot, and the minimum fill time.
- 705Sensitive fieldsEncryption before storage, who reads in clear and where, audited reads, exports, and why a file field cannot be sensitive.
- 706From submission to actionAn automation that runs once per submission, validation against live data, bursts and failures, and writes to other systems.
- 707Capstone: the supplier form end to endThe demo tenant’s supplier program assembled: form, page, table, intake automation, and a report joining applications to the warehouse.
The 700 series builds on the automations of the 600 series: an endpoint is an automation that answers a request, and a form is an endpoint that people fill in.
What makes an automation a form
A form needs two functions: one that says what the form is, and one that decides whether a submission is acceptable.
What makes an automation a form
form_spec(req)
- Called when someone opens the form. Returns the fields in order, any starting values, the confirmation shown on success, and the minimum time a person takes to fill it in.
form_submit(req, values)
- Called with a submission the platform has already checked against the spec. Returns nothing to accept, errors keyed by field to refuse, or its own confirmation.
the binding
- An administrator binds the automation to an endpoint that answers GET for the form and POST for a submission, as in 701.
A form is an endpoint whose automation defines these two functions in place of handle. It is drafted, tested, saved, and versioned like every other automation. There is no drag-and-drop builder: the agent writes the form, and a change to it is a new version with a summary the owner agrees to.
The fields
The agent turned the paragraph into twelve fields, each with a type and its limits.
The supplier-onboarding form’s twelve fields
company_name
text, required
- Up to 200 characters.
contact_name
text, required
- Up to 120 characters.
contact_email
email, required
- Must be an email address.
contact_phone
text
- Optional, up to 40 characters.
category
single choice, required
- The 50 product categories, read from the warehouse when the form opens.
pilot_store
single choice, required
- All 303 active stores, read from the warehouse when the form opens, labeled with city and state.
annual_volume_usd
number, required
- Between 1,000 and 50,000,000.
first_delivery
date, required
- A calendar date.
tax_id
text, required, sensitive
- Up to 10 characters, encrypted before it is stored. Lesson 705.
insurance_certificate
file, required
- One PDF or image, up to 5 MB, judged by its bytes rather than its name.
notes
long text
- Optional, up to 2,000 characters.
agree_terms
checkbox, required
- The supplier code of conduct.
Field types are text, long text, email, number, date, single choice, multiple choice, checkbox, hidden, and file. A form has at most 100 fields and a choice field at most 500 choices, so the full store list fits.
Choices from live data
Two of the fields offer choices a person picks from, and both lists come from the warehouse rather than from the form’s source.
Choices read from the warehouse
def form_spec(req):
"""The supplier-onboarding form, with its category and pilot-store choices read from the warehouse."""
categories = platform.query(
"SELECT CAST(category_id AS varchar) AS value, category_name AS label " +
"FROM warehouse.public.categories ORDER BY category_name",
connection = "acme",
)
stores = platform.query(
"SELECT CAST(store_id AS varchar) AS value, " +
"store_name || ' (' || city || ', ' || state || ')' AS label " +
"FROM warehouse.public.stores WHERE is_active ORDER BY state, city, store_id",
connection = "acme",
)
return {
"fields": [
...
{"name": "category", "type": "single_choice", "label": "Primary product category",
"required": True, "choices": categories["rows"]},
{"name": "pilot_store", "type": "single_choice", "label": "Pilot store",
"help": "The store you would deliver to first.", "required": True,
"choices": stores["rows"]},
...
],
"confirmation": "Thank you. The ACME vendor team reviews every application within five business days.",
"min_fill_seconds": 5,
}The spec is computed each time the form opens, so a store that opens next month or a new category appears in the form without anyone editing it. The page’s query string reaches the spec too, which is how a link can prefill a field.
Refusing a submission
The platform checks types and limits. Rules that belong to the business, such as the shape of a tax ID, belong to form_submit.
form_submit refuses field by field
def form_submit(req, values):
"""Checks the tax ID's shape and the code of conduct before accepting."""
errors = {}
if not valid_ein(values["tax_id"]):
errors["tax_id"] = "Enter the EIN as NN-NNNNNNN."
if not values["agree_terms"]:
errors["agree_terms"] = "The code of conduct must be accepted."
if errors:
return {"errors": errors}
return NoneTwo applicants on the demo tenant typed a tax ID without its dash and were answered 422 with the message beside that field; everything else they typed stayed on the page. Nothing was stored. Lesson 706 adds a check against live data to the same handler.
What the platform checks first
By the time form_submit runs, a submission has already passed five checks the automation never has to write.
What the platform checks before the automation sees a submission
- 1
A signed form token
The spec is signed into a token when the form opens, bound to this endpoint and good for a day. A submission without it, with an altered one, or with one from another form is refused 400. The platform keeps no copy; the submission brings its spec back.
- 2
A hidden honeypot field
The page renders a field named website that people never see. Only a program filling every field it finds fills it, and that submission is refused 400.
- 3
A minimum fill time
A submission sooner than the spec’s minimum after the form opened is refused 400. The supplier form sets five seconds.
- 4
Every field against the spec
An undeclared field, a required field left empty, a value of the wrong type, a choice that is not offered, a value past its limit, or a changed hidden field is refused 422 with each field’s error.
- 5
Then the automation
Only a submission that passed all of that reaches form_submit, with numbers as numbers, checkboxes as true or false, and files described by name, type, size, and address.
A public form adds a bot check on submission, covered in 704. On the demo tenant the form’s bot check, honeypot, and fill time each refused a test submission, with the messages shown there.
Binding the form
An administrator bound the form as a public endpoint with a bot check on submission, and the binding made a table for its submissions.
Binding the form makes its table
What the agent calls. This is the exchange the agent has with Plexara on your behalf, shown for the technical reader. You ask in plain language; you never type any of it.
→ manage_script
{
"command": "endpoint_set",
"name": "acme-supplier-onboarding",
"endpoint": "supplier-onboarding",
"methods": ["GET", "POST"],
"auth_mode": "bot_check",
"bot_check": "acme-public-forms",
"checked_methods": ["POST"],
"max_body_bytes": 10485760,
"connection": "scratch"
}
← {
"endpoint": "supplier-onboarding",
"form": {
"source": "form-supplier-onboarding",
"table": "webhook_form_supplier_onboarding",
"connection": "scratch",
"created": true
},
"message": "... Each accepted submission is stored as a row of
webhook_form_supplier_onboarding on connection scratch."
}Binding a form creates a source for its submissions and a table for that source. Each accepted submission is written as one event before the browser is told it was accepted, so a confirmation means the submission is stored. The page loads without a bot check and the submission is checked; the body limit is raised to 10 MB for the certificate.
The hosted page

Submissions as a table
Thirty-seven suppliers applied on the demo tenant. Each accepted application is one row, queryable at once.
Every accepted submission is a row
What the agent calls. This is the exchange the agent has with Plexara on your behalf, shown for the technical reader. You ask in plain language; you never type any of it.
→ trino_query
SELECT event_type,
json_extract_scalar(payload, '$.values.company_name') AS company,
json_extract_scalar(payload, '$.values.tax_id') AS tax_id_stored,
json_extract(payload, '$.sensitive') AS sensitive
FROM scratch.uploads.webhook_form_supplier_onboarding
ORDER BY received_at DESC LIMIT 3
← event_type company tax_id_stored sensitive
form_submission Brightleaf Tea Company encb:iuDbLfC7zeD/… ["tax_id"]
form_submission Summit Trail Outfitters encb:/m1DoqxCCX/… ["tax_id"]
form_submission Tidewater Seafood Partners encb:D8uxIvpp5NFZ… ["tax_id"]The table holds each submission’s values, the field order, which fields are sensitive, the endpoint and the automation version that accepted it, and on a form that requires sign-in, who submitted it. It is queried like any other table by anyone granted its connection. The tax ID is ciphertext here and everywhere else it is stored.
What a form does not do
A form on Plexara collects information from people into a governed table. These are outside what it does.
What a form does not do
No visual builder
- The agent writes the form from what you ask for.
One page, no conditions
- Every field is shown. There is no conditional logic and no multi-page flow.
No payments
- A form collects information, not money.
No respondent receipts
- The person sees the confirmation on the page. An automation that runs per submission (706) can post or deliver to your own channels.
About 10 MB per submission
- Files and fields together, across every file field.
Key terms
Four terms cover how a form is defined and where its submissions go.
Key Terms
- Form
- An endpoint whose automation defines form_spec and form_submit. The platform renders its page and checks every submission against the spec.
- Spec
- The fields, starting values, confirmation, and minimum fill time form_spec returns each time the form opens.
- Honeypot
- A field people never see. A submission that fills it is refused as a program’s.
- Form source
- The source binding a form creates, form-<endpoint>, whose table holds one row per accepted submission.
