Forms

CF7-style: write the HTML, declare a field schema, get validation, notifications, webhooks, and submission storage.

HTML you write, validation we wire

The forms extension is closer to Contact Form 7 than to a drag-and-drop form builder. You always write the HTML yourself. The admin gives you a small editor for the form's field schema (the metadata layer) and an HTML editor for the markup. The extension binds the two by matching name="<field>" attributes in your HTML to entries in the schema.

There is no visual form builder, and one is not planned. If you've used CF7, the mental model is: I write the form, the plugin handles submit, validation, notifications, storage.

Manage at /admin/forms. Each form has a name, slug, field schema, HTML body, notifications, webhooks, settings, and a submissions log.

Field schema

The schema is a typed list of inputs the form expects. Each entry describes one field by name and a type. The schema is what the rest of the system reads from — validation rules, conditional logic, notification placeholders, webhook payload shape, the submissions table.

Why a schema if HTML is custom?

Because the HTML alone is opaque — a raw <input name="email"> doesn't tell the server it's an email, that it's required, that it should be redacted from logs, or that it should be the recipient address on a notification. The schema carries that intent; the HTML carries the layout.

Schema field types

  • text, textarea, email, tel, url, number
  • select, radio, checkbox, multi_select
  • file (size + mime restrictions)
  • date, datetime, time
  • hidden, honeypot

Per-field config: name, label, required, default value, placeholder, help text, options (for select/radio).

The HTML body

You author the form markup directly. The extension renders your HTML as-is on the public site, wraps it in a <form> with the right action / method / CSRF, and binds inputs to the schema by their name attribute. You're free to use whatever classes, layout, and ARIA the rest of your theme expects.

form HTML (excerpt)
<div class="grid sm:grid-cols-2 gap-4">
  <label>
    Name
    <input name="name" required />
  </label>
  <label>
    Email
    <input name="email" type="email" required />
  </label>
</div>

<label>
  Message
  <textarea name="message" rows="6" required></textarea>
</label>

<input type="text" name="website" class="sq-honeypot" tabindex="-1" autocomplete="off" />

<button type="submit">Send</button>
HEADS UP
If a schema entry has no matching name="…" in the HTML (or vice versa), the form still works — but you lose validation / notification placeholders for that field. Keep the two in sync.

Conditional logic

Each field in the schema can have a conditions rule that decides whether it shows, is hidden, or is required. A rule is groups-of-conditions:

  • Matchall (AND) or any (OR) of the conditions in this group
  • Conditions{ field, operator, value } entries

Operators: =, !=, >, <, >=, <=, contains, starts_with, ends_with, is_empty, is_not_empty.

Multiple groups can combine. Conditional logic runs both client-side (live show/hide) and server-side (validation can't be bypassed).

Notifications

Each form can have any number of notifications that fire on submission. Per notification:

  • Subject — with {{field_name}} placeholder interpolation
  • Body — rich editor, same placeholders
  • Recipients — static list, from-field ({{email}}), or pulled from a role's email-subscriptions
  • From / Reply-To
  • Conditions — only fire if conditions met (e.g. only email sales when type = quote)
  • Email template — optional reference to an email-manager template instead of inline body

Notifications are delivered through the active email-manager provider — SMTP, Resend, or whatever is wired.

Webhooks

Per form, configure any number of webhooks. Each webhook is { url, method, headers, body_template, retries, backoff }. The body template renders the submission as JSON by default, or you can hand-craft the payload with the same {{field_name}} placeholders. Failed POSTs retry on configurable backoff.

Submissions

Every submission is stored in DB. Browse at /admin/forms/<slug>/submissions with search, filter, sort. Each row shows submitted values, IP address, user agent, source URL, timestamps, and notification + webhook delivery status. Export to CSV or JSON. Mark spam, restore, delete.

Spam & abuse

  • Honeypot field — declare a field with type: "honeypot" in the schema and place a matching hidden input in your HTML. Submissions where it's filled are silently discarded
  • Rate limiting — per-IP and per-session, configurable
  • Required-referrer — reject submissions not coming from your domain

Embedding a form

Two paths:

  • Form block — the forms-form content block; pick a form by slug from a dropdown in the block editor
  • Template helper{{ form "<slug>" }} renders inline in any layout, partial, or theme template