form-conventions
Skill130 starsChanged 7 days agoMITView on GitHub
Post-save conventions for Platypus frontend forms — where a save lands the user, what the submit button is called, how a failed delete is shown, and the unsaved-changes guard. Use when adding or changing a form in apps/frontend, its submit button, what happens after it saves, a delete confirm dialog, or a dialog's Cancel button.
In this repository38 other files it gives its agents.
- .agents/skills/humanizer/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- add-api-endpointSkill
- api-conventionsSkill
- ask-mattSkill
- card-conventionsSkill
- codebase-designSkill
- code-reviewSkill
- database-schema-changesSkill
- diagnosing-bugsSkill
- docs-auditSkill
- domain-modelingSkill
- grillingSkill
- grill-meSkill
- grill-with-docsSkill
- handoffSkill
- humanizerSkill
- implementSkill
- implement-specSkill
- improve-codebase-architectureSkill
- platypus-toolsSkill
- pre-release-checkSkill
- prototypeSkill
- prSkill
- researchSkill
- retroSkill
- setup-matt-pocock-skillsSkill
- tddSkill
- teachSkill
- to-questionnaireSkill
- to-specSkill
- to-ticketsSkill
- triageSkill
- ui-conventionsSkill
- wait-whatSkill
- wayfinderSkill
- wizardSkill
- writing-for-agentsSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 21
Read the shape off apps/frontend/app.
- Very specific: line 58
Every form built on useEntityForm is guarded for free: while its values differ from the ones it was seeded or last saved with (isDirty), a …
- Very specific: line 83
Every entity form is a real <form> whose onSubmit runs the same save the footer button does: preventDefault(), then the form's submit.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Frontend Form Conventions
Where a save lands
One rule covers every form: a save returns the user to the entity's home, and only leaves the page when the form has no home to return to. Which of the two applies falls out of the entity's shape in the route tree:
| Shape | The form is… | On save | Examples |
| Collection member | the entity's page, reached from a list | go to list | Agents, Blueprints, Skills, Triggers, MCP, Providers, Webhooks, Workspace Context |
| Entity sub-page | a settings/ child of a page the entity owns | stay | Boards |
| Singleton | one per Workspace/Org/account, so there is no list | stay | Sandbox, Organization, Workspace |
| Dialog or inline | not a page at all | stay | Members, Invitations, Global Context, everything in Chat |
Read the shape off apps/frontend/app. settings/agents/[agentId] renders
the agent edit form — an agent has no detail page, so leaving the form means
returning to settings/agents. A board does have a page, and its form lives
at boards/[boardId]/settings, so saving keeps you there.
Staying means mutate() (or router.refresh()) plus a toast. Never
router.push to the page you are already on.
Creating always navigates — to the new entity where it has a page of its own, otherwise to the list it now belongs to.
Deleting always navigates away from whatever it destroyed, whatever the shape: to the list for a collection member, to the parent otherwise.
A list is wherever the list actually renders, not a route that looks like one.
Triggers navigate to the Workspace home because that is where the trigger list
lives; there is no triggers/ page.
When a delete fails
A refused delete is shown inline, in the confirm dialog, which stays open.
The spinner stops and the user can read the message, then retry or cancel.
This holds for every delete, form or list: use useEntityDelete or
useDeleteFlow and pass the error to the dialog. Never toast a failed delete
and close the dialog.
The one exception is guidanceOnForbidden, on both hooks, and it is for
Shared-resource deletes only. Deleting a Shared resource from a Workspace is
refused as forbidden, and the message only says where the resource is
managed: guidance, not an error the user can act on in this dialog. With the
option set, a forbidden refusal closes the dialog and shows the message as
an info toast. Every other refusal still stays inline.
A dialog's Cancel button is variant="outline", never ghost.
Leaving with unsaved changes
Every form built on useEntityForm is guarded for free: while its values
differ from the ones it was seeded or last saved with (isDirty), a
same-tab link click opens "Discard unsaved changes?" and refresh or tab
close shows the browser's prompt. Typing then reverting is clean. A
successful save is clean before onSuccess runs, so the post-save redirect
goes straight through; a failed save stays dirty.
- A value the form sets itself — a default once options load, a reset after
save — goes through
seedFormData, notsetFormData, or the untouched form reads as dirty. - An edit the form keeps outside
formData(a selection, raw JSON text, an avatar file) goes insideState, or changing it alone never prompts. - A deliberate full document load after a write uses
hardNavigate, notwindow.location.href, so the browser's prompt doesn't fire over it. - A surface with its own state and no
useEntityForm(the Card dialog) callsuseUnsavedChangesGuard(isDirty)and routes its own close through theconfirmDiscardit returns. - A small dialog or inline action built on
useEntityForm(member edit, chat rename, issue token) passesguardUnsavedChanges: false: it is not a form the user would lose work in. - Programmatic navigation (
router.pushafter save or delete) and browser Back are never held. The App Router cannot block Back.
How the form submits
Every entity form is a real <form> whose onSubmit runs the same save the
footer button does: preventDefault(), then the form's submit. The footer
passes type="submit" to FormFooterButtons, so Enter in any single-line
field saves once.
- Give every other button inside the form an explicit
type="button"— Advanced settings, reveal/copy, Add model, Test connection, Regenerate, and a draft step's Back/Next. An untypedButtondefaults to submit. - A draft step in the Workspace wizard keeps its own Back/Next buttons and
steps through
draft.write; the wizard must not save twice. - A dialog that renders its own
<form>(the Add/Edit model dialog) stops the submit event, because React bubbles it through the portal to the surrounding form. - A search field must not submit the form around it.
- Set
noValidateon the form. The shared schemas own validation and their messages, so the browser's step/type bubbles must not block a save.
What the button says
The label names the outcome, so it follows from the destination:
| Case | Label |
| Create, page-level form | Save |
| Edit that navigates to the list | Update |
| Edit saved in place | Save |
| Adding to a surface you stay on | Add |
Update is reserved for the edit-then-leave case — it is the word for a save
you are about to be carried away from. Add covers the in-place additions:
Add column, Add card.
Let the form heading name the entity and keep the button to the bare verb
(Save, not Create board). Prefer the bare verb over Save changes too. A
loading state keeps the same verb as the button it replaces, with the repo's
ASCII ellipsis: Saving... under Save, Updating... under Update.
Unlike the destination rule, this one is ahead of the code. Most forms today
label on {id ? "Update" : "Save"} alone, so a stay-in-place edit can still
read Update (Sandbox, Organization, Workspace); #793
brings the existing buttons into line. Write new buttons to the rule above,
and leave the stragglers to that issue.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

