/form-authoring
The technical reference the agent reads before writing or editing a form.
/form-authoring is the spec for everything under forms/.
/create-form,
/create-theme and
/apply-comments defer to it, and AGENTS.md
tells the agent to read it before any edit inside forms/ — so "add a
phone field" or "make this optional" goes through it too.
You don't usually invoke it directly. Other skills and the rules do.
What it covers
| Topic | What the agent learns |
|---|---|
| File contract | forms/<id>/index.tsx, one <Form>, header text in config, a default export named after the form, render-safe code (the form renders once on the server to derive its schema). index.tsx reads as the whole form: config, every <Page> and <When>, and every field's name and label stay there; other files only for large components or option lists, in a flat folder with theme/ and locales/ as the only subfolders |
| Structure | <Form>, <Page>, <When>, <Row>, <Heading>, <Paragraph>, <Submit>, <Success> |
| Fields | Every field, what it stores, and its props |
| Conditions | <When> operators (and what they mean on a CheckboxGroup), all / any / not / condition, conditional steps, hidden answers dropped on the server |
| Validation | Built-in rules, validate functions or a Standard Schema, run on client and server alike |
| Custom fields | useField and FieldShell |
| Live answers | useFieldValue, useFormValues and <Form aside> for previews and totals; the hooks are called at a component's top level only |
| What Open Form can't do | File uploads, self-scoring quizzes, repeating groups, emails to respondents, sign-in, payments and editing a response after submitting: why each doesn't work, the workaround to offer (retake limits with uniqueBy: 'email' need a required email on a step everyone sees; repeating groups get one cumulative <When> range per position), and that the user hears about it before the build |
| Custom styling | A local stylesheet scoped with <Form className>, prefixed classes on --of-* tokens, --of-aside-width, and the .of-* classes to target when restyling what the runtime renders |
| Config | Submission policy, protection, success screen, drafts, time zones, published: false to take a form offline, and the keys Studio can override; webhooks and embeds in open-form.config.ts |
| Themes | The form's own look in forms/<id>/theme/ and <Form theme>, written with only what the form uses — tokens, then CSS on the runtime's markup, then components for what CSS can't do; references in themes/ and the starters, create --theme, replaceable components, layouts and a custom Layout example (keeping the language picker's corner clear), closed screens (Notice), useFormNav and useFormMessages |
| Languages | locale, translations, <T>, language-neutral option values |
| Design guidelines | Questions: ask less, order easy to hard, 3–7 fields per page, the step count and time up front, data entry on one page, RadioGroup vs. Select vs. cards. Look: each header fact once, one signature element tied to the form's purpose (best on the key question's control, echoed on the success screen), no AI-default looks (cream + serif + terracotta, black + neon, # and → as decoration), the theme's palette only, Chinese and Japanese type without Latin uppercase or italics, the first question on a phone's first screen, success copy that uses the person's answers. Composition: ambition sized to the moment (a plain column for a quick QR card, a full composition for an invitation), one question per screen for NPS and quizzes, answers in one column, but the page around them chosen for the form — centered card, hero, full-page poster, document, sticky summary, step rail or one question per screen |
The Components and Reference sections document the same API for people.
Anti-patterns
The skill ships a table of things agents reach for by habit, and what to write instead:
| Don't | Do |
|---|---|
useState + onChange for values | Field components, or useField |
{watch('x') === 'y' && …} or ternaries on values | <When field="x" equals="y"> |
onSubmit={() => fetch('/api/…')} | Nothing — submission is built in |
useFormValues() to show or hide fields | <When>; the hooks are for display only |
| API routes, SQL, Express handlers | config.submissions / protection |
fetch() to Slack or Zapier on submit | webhooks in open-form.config.ts |
Hand-written if (!email.includes('@')) | <Email>; validate for real custom rules |
theme: 'terminal' in defineForm, or importing from themes/ | The form's own theme/index.ts (open-form create <id> --theme terminal) + <Form theme={theme}> |
| Copying a reference theme's whole folder into the form | Only what the form renders: tokens, then CSS on the runtime's classes, then components that need their own markup or behavior |
| CSS grid or flex to arrange fields | <Row>, <Page>; grid and flex belong in the Layout, around the field column |
| Every form a centered card at the default width | A composition chosen for the form |
| English uppercase or italic styling on Chinese or Japanese text | Upright CJK text, weight and size for emphasis, Latin styling under :lang(en) |
Styling tags (button[type="submit"], h1) or unscoped .of-* rules | The .of-* classes under [data-of-theme="<id>"] |
| Hex colors in the form's CSS | var(--of-*) tokens; tints via color-mix() |
| The date and venue in the description and in an aside | Each fact once |
| A bare "Thanks!" success screen | What happens next, in the form's voice |
Raw <label> / <input> markup | DSL components (they handle ARIA, focus, errors) |
| Renaming a field that already has responses | Keep the name; change the label |
Reading window during render | useEffect |
Self-review checklist
Before handing off, the agent checks:
- Ids — the folder is kebab-case; every
nameis unique, camelCase and meaningful. - Required and conditional — only truly necessary fields are
required; conditional fields use<When>, not ternaries. Anything the brief asked for that Open Form can't do was raised with you, and the workaround is listed at hand-off. - Policy — limits, dates, uniqueness and language match the request, and nothing more was added.
- Nothing hand-rolled — no state, fetch, validation or styling outside the theme and form CSS.
- The look was chosen — the form's own look in
forms/<id>/theme/follows the agreed direction and holds only what this form renders, with no componentcheckreports as unused, imported above the form's CSS and passed to<Form theme>; nothing imports fromthemes/; copy follows the reference'sTHEME.md; conversational forms have one question per page. Each header fact appears once, there's at most one signature element, the form's CSS adds no new hues, the success copy is written for this form, the composition was chosen for the form, and Chinese or Japanese text has no uppercase, small-caps or italic styling. - Custom components spread
field.controlProps, attachfield.ref, keep native inputs and a visible focus style. open-form check <id>passes and its steps, fields, conditions, required flags and policy match the plan; thetypecheckscript passes.- Closed screens — the messages are in the form's voice, and a custom
Noticehandles every state the form can reach without inventing facts. - Translations —
localeis set, options have explicit values, every language showsN/N translated, and every "not seen" entry is text shown only for certain answers or avalidatemessage, not a key left behind by edited text. - Seen, not assumed — screenshots of
/f/<id>at desktop and phone width match the plan: the title shows, the form's own CSS applies, nothing is low-contrast or clipped, nothing sits under the language picker on a translated form, and the first question fits a phone's first screen. Then Studio's Preview: conditional pages through the step buttons, each closed screen under When closed, and each translation through the language picker. Without a browser tool, the agent says it hasn't seen the form.