Forms for WordPress with every premium feature free — built as an OpenStation desktop app with a drag-and-drop builder, ten themes, and no paywall anywhere in it.
A forms plugin. You drag fields onto a canvas, pick a theme, and put the form on a page. People fill it in; you read what they said.
Three decisions shape everything else.
Conditional logic, calculations, multi-page forms, file uploads, signatures, repeaters, save-and-resume, entry management, CSV export, conditional notifications, webhooks, surveys, quizzes, user registration and front-end post submission are the paid tier of every other forms plugin. None of them is technically hard. They are all here, in the free plugin, and the only reason they were ever sold separately is that somebody could.
Building a form is a spatial act — you drag a field from a palette and put it
somewhere. Rendering into the shell's own DOM is what gives the builder
wp.os.dragManager, the same pointer pipeline the desktop's file tiles ride. So:
| Drag this | Onto this | And |
|---|---|---|
| A field from the palette | The canvas | It is added where you dropped it |
| A field already on the canvas | Somewhere else on it | It moves |
| A field | A second builder window | It is copied into that form |
| An image from WP Explorer | An image-choice option | It becomes that option's picture |
| An entry from the Entries window | An AllTerrain Work column | It becomes a task |
None of that is reachable from inside an iframe. It is also the reason the
Entries window emits allterrain-forms/entry as a public payload type: any other
plugin can register a drop target for it and decide what a submission means to
them.
A form is a post, an entry is a post, an entry note is a comment, a saved theme
is a post. Nothing lives in a bespoke table, so the REST API,
current_user_can(), revisions, search, the trash, and WordPress's own privacy
exporter and eraser already work on this data without a line of integration code.
OpenStation is required. The plugin declares Requires Plugins: desktop-mode, so WordPress installs and activates the shell alongside it
and refuses to pull the shell out from underneath it. The builder is a native
shell window — the desktop is the product, not a skin on it — so the plugin
does not ship a lesser version of itself for sites without one. The
function_exists() gates in includes/shell-api.php remain as defense in
depth for half-upgraded sites: without the shell an admin notice says what is
missing, and published forms keep rendering so no visitor ever pays for an
admin dependency.
OpenStation has a convention for previewing: a window with something to show on the front end carries an eye on the right of its title bar, and pressing it opens the front end as its own window, paired with the editor. The shell does this for post and page edit screens. A form is the same shape of thing, so it wears the same affordance.
Press it and the form opens beside the builder as a real front-end render — same renderer, same stylesheet, same theme resolution, so what you see is what a visitor gets. Save, and the preview window refreshes itself. Drag a field, watch it change, drag another; the builder never closes.
A theme is one flat map of design tokens and nothing else. There is no theme PHP, no theme template, no theme stylesheet. The renderer emits the same markup for every theme and the tokens decide what it looks like — which is what makes "expandable without code" true rather than a line in a README.
| Clean — the neutral default | Midnight — dark, high contrast | Glass — translucent, real backdrop blur |
| Brutal — 3px black rules, offset shadow | Paper — warm stock, serif headings | Neon — near-black, gradient accents, glow |
| Terminal — monospace, phosphor green | Soft — neumorphic, dual inset shadows | Editorial — big serif, labels in the margin |
| Holo — the OpenStation brand |
Sixty-nine tokens across eleven families: colour, radius, shadow, border, spacing, typography, label position, button shape, focus ring, motion and effects. A theme can change the corner radius of the checkbox independently of the text field, put labels inside the field or in the margin, swap the field for an underline, add a backdrop blur, or paint the submit button with a gradient.
Theme Studio is a native window with a control per token and a live preview. Duplicate a built-in, move sliders, name it, save — it becomes a theme selectable anywhere a built-in is, exportable as JSON and importable on another site. No code, no build step.
The dials are the part worth explaining. Sixty-nine tokens is a complete
description of a theme and a terrible way to make one: nobody thinks "I want
radius-field: 14px, radius-button: 14px, radius-card: 24px" — they think
rounder. So six controls each drive a whole family — Accent, Roundness,
Density, Depth, Fields, Labels — and the full token list stays behind Advanced
for the cases they cannot express. Both write the same token map; the dials are a
faster way to write it, not a parallel format.
The token surface and the stylesheet are asserted to agree in both directions by the test suite: a token no CSS rule reads is a control that does nothing, and a custom property no token declares is a value no theme can reach. All ten themes are also checked for WCAG AA contrast.
Text — single line, paragraph, email, website, phone, number, password, hidden Choice — dropdown, multi-select, radios, checkboxes, image choice, toggle Date & time — date, time, date & time, date range Advanced — file upload, signature, star rating, opinion scale, Likert matrix, slider, colour, name, address, country, repeater Layout — section heading, HTML block, divider, spacer, page break Special — consent, calculated total, quiz question
Every one of them is a single alltfo_register_field_type() call using exactly the
API a third-party plugin would use. There is no privileged path: if a built-in
needs something the registry cannot express, the registry gets a feature rather
than the built-in reaching around it.
A LOGIC badge tells you a field has a condition and nothing about what it is.
To find out you had to select the field, scroll the inspector, and read three
dropdowns — and even then you learned about that one field, not about the shape
of the form. A form where three questions depend on the first one has a
structure, and a flat list is not showing it.
So every card states its condition, and the controller is joined to what it controls by a labelled curve. Hovering a card dims every curve it does not touch, which turns "what does this field affect?" from a search into a glance.
The condition is drawn as separated parts, not a sentence:
SHOWN WHEN· Can you make it? · is ·Yes, I will be there
Both the question and the answer are text somebody typed, so the question ends in a question mark and the answer contains a comma — the punctuation a sentence would rely on for structure is inside the content. Chips remove the parsing. The question chip is a button that selects that field, and a rule pointing at a deleted question is drawn in red, because that form is genuinely stuck and it was previously invisible.
{field:f2} asks somebody to know three things nobody told them: that braces mean
something, which tags exist, and that their second question is internally called
f2. It sat in the two panes that decide whether anyone ever finds out a form was
submitted.
Every box that takes tags has an Insert a value picker listing the form's own questions by the label you wrote, grouped with the submitter, the submission, the form and the site — each row showing what it actually resolves to on this site. Under the box, a "reads as" line with the tags filled in.
Send it to is asked in plain language too: whoever runs this site, the person who filled it in (naming your email question), a specific address, or free text for the rest. The common cases need no tags at all.
The catalogue is served from PHP — alltfo_merge_tag_catalogue() — so it cannot
drift from what actually resolves, and the suite fails if it ever advertises a tag
the resolver does not know.
Logic — conditional show/hide on any field, page, notification, confirmation
or post-submit action, with all/any matching and chained conditions. Calculations
over field values with a safe expression evaluator (no eval, ever). Multi-page
forms with step validation. Save and resume. Pre-fill from the URL, the logged-in
user or the date. Unique-value validation.
Anti-spam without a captcha — honeypot, signed time trap, per-address rate limit, word blocklist, and Akismet — off by default — when the site already has it and switches it on per form. Nothing asks the visitor to prove they are human, because that charges them for the site's spam problem and fails WCAG for a good number of people. A submission judged spam is stored, in a spam status, never silently discarded.
After submission — multiple conditional notifications with merge tags, CC/BCC, reply-to and attachments. Multiple conditional confirmations: a message, a URL, or a page. Webhooks with HMAC-SHA256 signing. Actions that create a post, register a user, or write user meta — each with a capability ceiling that a form author cannot raise.
Managing — an entries table with search, filters and star; a detail view; bulk read/spam/trash; CSV export with formula injection defused; survey and quiz reporting.
Reporting — an analytics window with views, starts, conversion and completion; submissions per day; a tally or a distribution per question; Net Promoter Score for any 0–10 question; and a cross-tab that breaks every numeric answer down by any categorical one — which is the difference between "the average score is 7.5" and "Support scores 5.8 and everyone else is above 7".
Privacy — a retention policy that deletes automatically, IP anonymisation, and integration with WordPress's own export and erase tools so a subject access request reaches form submissions without anybody having to remember they are a separate place.
The plugin registers its whole working surface with WordPress's Abilities API, which is how a site describes what it can do to agents and MCP clients. Each ability is a thin adapter over the same functions the windows and the REST API call — an agent and a human clicking the same button get the same behaviour, the same validation and the same capability checks.
| Ability | What it does |
|---|---|
list-forms |
Every form with id, title, theme, shortcode, entry count and questions — the ids feed everything else |
get-form |
One form's questions: field ids, types, labels, required flags, choices |
list-field-types |
The building vocabulary: all 37 types and what each stores |
create-form |
Builds a form from a title and a loose field list; returns the shortcode |
set-form-theme |
Dresses a form in any installed theme |
submit-form |
Submits through the visitor pipeline — validation, anti-spam, storage, notifications; refusals return per-field errors |
list-entries |
Queries submissions — search, date range, status, pagination — raw and human-readable |
get-entry |
One submission, every answer labelled and formatted |
form-report |
The analytics as structured data: conversion, timeline, distributions, NPS, group-by breakdowns |
Ask an assistant to "make a booking form and show me last week's responses",
and these are what it calls. The full contract — permissions, schemas, and the
one honest liberty submit-form takes with the time trap — is in
docs/abilities.md.
Not a section that says "we care about accessibility". The things that were actually done:
- Every control has a real
<label>bound byfor. Grouped controls (radios, checkboxes, ratings, scales, names, addresses) are in a<fieldset>with a<legend>, because a<label>may only point at one control and one above six radios is bound to none of them. - The Likert matrix is a real
<table>withscopeon both header directions, so each radio's accessible name is its row's statement crossed with its column's answer. - Hints and errors are wired with
aria-describedby; invalid fields carryaria-invalid. The error summary is a focusablerole="alert"region that takes focus on a failed submit. - Required is announced with
required, not with an asterisk — the asterisk isaria-hidden, because "asterisk" tells a screen-reader user nothing. - A required field hidden by conditional logic is not validated. Its controls are disabled as well as hidden, so the browser cannot block submission on a field nobody can see or reach.
- The builder is keyboard-complete: every palette entry is a
<button>that adds its field, and Alt+↑/↓ moves a field on the canvas. Dragging is never the only way to do anything. prefers-reduced-motionandforced-colorsare both honoured. All ten themes meet WCAG AA contrast, and the suite fails if one stops.- The form works with JavaScript switched off: a real
POST, real server validation, errors against the right fields, answers still in place.
git clone https://github.com/allterraindeveloper/forms.git
cd forms
npm install
npm run buildDrop the directory into wp-content/plugins/ and activate it. With OpenStation
installed the builder appears as a desktop window and a wallpaper icon; without
it, under Forms in the admin menu.
Place a form with the shortcode the builder shows you:
[allterrain_form id="12"]
…or with the Form block, which has a picker and a live preview.
| Command | What it does |
|---|---|
npm run build |
Builds every bundle, dev and minified, then mirrors into a local site |
npm run build:titlebar |
Builds the small title-bar preview provider, dev and minified |
npm run dev |
Rebuilds the builder bundle on save |
npm run typecheck |
tsc --noEmit |
npm test |
Vitest — the shared conformance suites |
npm run env:start |
Starts a WordPress via wp-env |
npm run test:php |
PHPUnit inside wp-env |
npm run test:php:local |
PHPUnit against a WP_TESTS_DIR you already have |
npm run plugin:build |
Typecheck, test, build — everything a release needs |
npm run plugin:package |
The above, then dist/allterrain-forms.zip |
npm run env:start brings up a WordPress with this plugin mounted. .wp-env.json
also mounts a sibling ../alcazaba-plugin checkout so the desktop shell is there
to host the windows — without it you get the plugin's admin-page fallback, which
is a complete forms plugin and not the interesting half.
npm run build ends by mirroring the built tree into a local WordPress checkout
if it finds one (../wordpress-alcazaba or ../wordpress-develop). Override with
ALLTFO_DEPLOY_TARGET, or skip with ALLTFO_SKIP_DEPLOY=1. It is a convenience, not a
build requirement: on CI it finds nothing, says so, and exits successfully.
Four places carry the version and they must agree:
| Where | What |
|---|---|
allterrain-forms.php |
the Version: header |
allterrain-forms.php |
ALLTFO_VERSION |
readme.txt |
Stable tag |
package.json |
version |
Bump all four, commit, then tag:
git tag v0.2.0
git push origin v0.2.0The tag starts release.yml, which checks the
tag against all four, builds and packages, runs the PHP suite, creates a GitHub
Release with the zip attached, and deploys to WordPress.org.
A tag whose version disagrees with any of the four fails before the Release exists. That ordering is the point: a bad commit can be replaced, and a published tag cannot.
A prerelease (v0.2.0-rc1) stops at the GitHub Release. WordPress.org has no
concept of one and pushing it would make it the live download.
The WordPress.org step needs SVN_USERNAME and SVN_PASSWORD repository
secrets, and cannot succeed before the plugin is approved and its SVN repository
exists. Until then the first submission is a manual zip upload; everything before
that step is useful from the first tag on.
Re-running a failed deploy is a manual dispatch rather than a re-run:
gh workflow run release.yml --ref main -f tag=v0.2.0A tag push runs the workflow definition frozen into that tag's commit, so a
deploy that failed for a workflow-level reason can never be fixed by re-running
it. Dispatching from main runs the current definition and leaves the existing
Release alone.
ci.yml runs three jobs on every push and pull
request:
| Job | What it proves |
|---|---|
js |
Types, Vitest, and that the committed assets/js bundles match their source |
php |
PHPUnit and PHPCS, inside wp-env |
package |
That npm run plugin:package still produces a zip, uploaded as an artifact |
The third exists because a packaging script that only ever runs at release time
is a script that breaks at release time. The first exists because assets/js is
committed on purpose — it is what makes "download the zip from a tag and install
it" work with no npm install — and that only holds while the committed bundles
match the TypeScript they came from.
.wp-env.json mounts a sibling checkout that does not exist on a runner, and
wp-env treats a missing mapping as fatal, so both PHP jobs write a
.wp-env.override.json that replaces mappings wholesale. Nothing is lost: every
shell call in this plugin sits behind alltfo_shell_has(), which is simply false
when no shell is installed.
The conditional-logic and calculation engines exist twice — once in PHP, once in TypeScript — because the browser hides and shows fields as the visitor types and the server decides what was actually required. If they disagree, the visitor is shown a form they cannot submit, with an error about a field they cannot see.
So they are not tested twice. tests/fixtures/logic-cases.json and
calc-cases.json hold one table each, and both suites run it:
tests/fixtures/logic-cases.json ──┬── tests/vitest/logic.test.ts
└── tests/phpunit/tests/logic.php
A case added to one language is a case added to both, and a change that breaks the parity fails in both suites. 113 shared cases, plus each side's own.
npm test # 166 TypeScript tests
npm run test:php # 465 PHP tests, inside wp-envdocs/hooks-reference.md— every action and filterdocs/abilities.md— the nine WordPress Abilities AI agents use the forms through: build, theme, submit, query, reportdocs/field-types.md— adding a field typedocs/themes.md— the token surface, and adding a tokendocs/javascript.md— events, drag payloads, the bundlesdocs/architecture.md— how it fits togetherdocs/openstation-requests.md— what this plugin needed from the shell, what it got, and what is still missing
GPL-2.0-or-later. Every premium feature, free, forever.
Use Export in the builder to download the current form with its base theme, only changed theme/advanced CSS settings, all field/settings/logic/action configuration and embedded image-choice attachments. Validate YAML checks a file without writing anything. Import accepts YAML or versioned JSON and creates a new draft with its own theme.
For LLM-assisted editing, use the format guide, JSON Schema and example YAML. Validate a document locally with:
npm run validate:form -- path/to/form.yamlThis checks the portable contract offline. Import also checks installed field types, safe theme tokens and image contents. Entries and external plugin/site configuration are separate from the form definition; see the guide for dependency handling.
The browser round-trip test runs against the local wp-env site with OpenStation and AllTerrain Forms active:
npx playwright install chromium
npm run env:start
npm run test:e2eATF_E2E_URL, ATF_E2E_USER, and ATF_E2E_PASSWORD override the default local URL (http://localhost:8889) and admin / password test account. The test creates its own form/theme and deletes them afterward. On an older development database, reactivate AllTerrain Forms to refresh its capabilities first.
The phone builder regression also requires OpenStation enabled for the test user.
It checks tap-to-add, the full-width inspector, card bounds, saving and reopening,
and the transition back to the desktop layout. Run it on its own with
npx playwright test tests/e2e/builder-phone.spec.ts.
With OpenStation's compatible window MIO API, open AllTerrain Forms and use Ask MIO to create a draft or update the current form. YAML is validated before saving; invalid candidates receive precise errors and up to two correction attempts. Existing forms keep their publication status. Manual and file workflows remain available in classic admin. Provider setup and MIO enablement belong to OpenStation.
- Detailed linked Markdown knowledge base
- Developer handoff: review of MIO PR #816
- Conditional contact YAML example
Browser tests default to Docker http://localhost:8889 (npm run test:e2e). The MIO
adapter cases capture the real shell registration and simulate model tool calls.
The chat case uses the real composer, session and rendered Preview button, replacing
only provider responses. All use real WordPress validation/saves without invoking
a paid AI provider.
The recovery MIO API adds turn-wide correction limits, compact document history
and read-only reconciliation of uncertain saves. To exercise the adapter against
an actual local MIO checkout, run
ATF_MIO_SOURCE=../alcazaba-plugin npm test -- --run tests/vitest/mio-contract.test.ts.
The response button handoff records the plan
and implementation follow-up. Forms supplies a Preview action after a confirmed
save when the shell supports the new optional responseActions API.







