Skip to main content
Plexara
Product11 min read

703 - Your first form

Ask the agent for a supplier-onboarding form and it writes one: a form_spec that declares twelve fields, with category and pilot-store choices read from the warehouse each time the form opens, and a form_submit that refuses a malformed tax ID field by field. What the platform checks before the script sees a submission, the field types and their limits, the hosted page, and every accepted submission as a row in a table. The limits are stated where they arise: no conditional logic, no multiple pages, no payments, no respondent receipts, and no visual builder.

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

  1. 01Ask the agent for a form and read what it writes: a spec and a submit handler.
  2. 02Choose field types and limits, and fill choices from live data.
  3. 03Refuse a submission field by field from the submit handler.
  4. 04List the checks the platform makes before the form’s automation sees a submission.
  5. 05Find the hosted page and the table every accepted submission lands in.
  6. 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

Open index

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 None

Two 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. 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. 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. 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. 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. 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

A hosted form page, partly filled: required company name, contact email and product category, a list of stores to deliver to read from the store table, a required tax ID marked as encrypted when stored, a file field for a signed W-9, and a Submit button.
The page the platform serves for a form. The store choices are read from the warehouse each time the page opens.

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.