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,numberselect,radio,checkbox,multi_selectfile(size + mime restrictions)date,datetime,timehidden,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.
<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>
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:
- Match — all (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-formcontent 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