Outcome
Change the approved form definition, including explicit unpublishing, while preserving concurrent-write and public-content boundaries.
PATCH /api/forms/{formId}
Before you start
Use the original authorized organization and discovered canonical Forms workspace, bound separately from input. Discover the real form through an authorized read when applicable. The published action requires workspace:write and the selected route requires read_write workspace access; sample ids are not grants.
Obtain approval for the actual mutation, target content and described effects. Validate the exact current action input and confirmed executable plan before a real call. This guide and its fixtures authorize no form change, public exposure, deletion, submission or notification.
All examples are complete source-derived synthetic responses, unrun. No real form/subject/protection/key/mirror/content/submission/provider/model/delivery/notification operation was executed. Source declaration matches the reviewed immutable revision; loaded runtime, dependency health, public rendering and actual delivery remain unverified.
Request and effects
Action: app_topolo_forms.forms.update.
Requires formId, workspace:write and verified read_write workspace access. Resolve the actual form through authorized list/get and preserve a reviewed preimage. A scoped missing row returns404. The current status input accepts only draft; publishing requires the separate publish action.
Omitted title,fields,thank-you content,slug and status use values from the initially read model. Supplied fields are normalized as a replacement array; the current schema and handler reject an empty array. Omit fields to retain the stored definition. A blank supplied description falls back to the existing description and cannot clear it through this branch. Blank thank-you values instead use translation defaults.
This is a read-modify-write over multiple protected columns, not a version-guarded atomic field patch. No expected version/ETag compare is shown, so a concurrent edit can be overwritten even when an input field was omitted. The final model is read separately and may reflect later changes.
When status is omitted on a published form, it stays published and the stored definition changes. A later public read can consume that changed definition without another publish action; global edge/cache freshness remains unverified. Explicit status:draft clears publishedAt, and the reference event is classified unpublished when the prior status was published.
The route schedules a forms.form.changed Connected Apps reference event after the update/read-back when a form is returned. It uses the actual actor or owner and requires configured Notify/service identity. The event is not awaited as a delivery receipt; failures are caught/logged and do not undo the stored edit. No real provider propagation or public rendering is proved by HTTP200.
Complete worked responses
Edit a published definition in place
Complete source-derived synthetic, unrun HTTP200. Assumes a matching readable published fictional form equal to the prior read example, successful protection/update/read-back and shown clock. All other stored values remain as shown. No actual edit or reference event was sent.
Input:
{
"formId": "forms_docs_fixture",
"title": "Updated fixture feedback"
}
Response:
{
"forms": {
"id": "forms_docs_fixture",
"organizationId": "org_docs_fixture",
"workspaceId": "ws_forms_docs_fixture",
"title": "Updated fixture feedback",
"description": "Synthetic documentation example",
"thankYouTitle": "Thank you",
"thankYouMessage": "Your fixture response was received.",
"slug": "fixture-feedback",
"status": "published",
"fields": [
{
"id": "topics",
"label": "Topics",
"type": "multi_select",
"required": false,
"options": [
"Docs",
"API"
],
"width": "full"
}
],
"createdByUserId": null,
"createdByEmail": null,
"createdAt": "2026-10-01T08:00:00.000Z",
"updatedAt": "2026-10-21T01:00:00.000Z",
"publishedAt": "2026-10-02T08:00:00.000Z",
"submissionCount": 3
}
}
Retain actual original target/request evidence and perform the verification/recovery below. The fixture is not a live outcome or authority.
Explicitly return the form to draft
Complete source-derived synthetic, unrun HTTP200. Assumes the same original published preimage and successful scoped update/read-back. publishedAt becomes null. This response does not certify edge invalidation, downstream event delivery or retraction of already submitted data.
Input:
{
"formId": "forms_docs_fixture",
"status": "draft"
}
Response:
{
"forms": {
"id": "forms_docs_fixture",
"organizationId": "org_docs_fixture",
"workspaceId": "ws_forms_docs_fixture",
"title": "Fixture feedback",
"description": "Synthetic documentation example",
"thankYouTitle": "Thank you",
"thankYouMessage": "Your fixture response was received.",
"slug": "fixture-feedback",
"status": "draft",
"fields": [
{
"id": "topics",
"label": "Topics",
"type": "multi_select",
"required": false,
"options": [
"Docs",
"API"
],
"width": "full"
}
],
"createdByUserId": null,
"createdByEmail": null,
"createdAt": "2026-10-01T08:00:00.000Z",
"updatedAt": "2026-10-21T01:00:00.000Z",
"publishedAt": null,
"submissionCount": 3
}
}
Retain actual original target/request evidence and perform the verification/recovery below. The fixture is not a live outcome or authority.
No matching form
Complete source-derived synthetic, unrun HTTP404. Assumes original auth/workspace/write checks pass and no scoped row exists; no content update is reached.
Input:
{
"formId": "forms_absent_docs_fixture",
"title": "Updated fixture feedback"
}
Response:
{
"error": "not_found"
}
Retain actual original target/request evidence and perform the verification/recovery below. The fixture is not a live outcome or authority.
Verify and recover
-
Read the actual form in its original workspace and compare the intended postimage with the reviewed preimage. Reconcile concurrent changes before repeating; there is no published optimistic concurrency token here.
-
For a published edit or explicit draft transition, verify the approved public/Connected Apps effect separately if required. Do not submit a real respondent response or infer all caches and recipients changed from the mutation response.
Only with a reviewed retained preimage and current-state reconciliation, plan a separately approved compensating update. Timestamps, encryption/key effects and emitted reference events are not reversed. Republishing after draft requires the publish action and renewed effect assessment; never overwrite someone else’s later edit automatically.
Refusals and uncertainty
400 / workspace_required: Discover and bind the canonical authorized workspace through supported Auth resource/workspace actions, not a guessed id or app-bootstrap grant.
403 / forbidden: Preserve the actual write permission refusal. Seek legitimate access through its owner rather than broadening context.
404 / not_found: Reconcile the real discovered form in the same original workspace; for uncertain prior writes/deletion, absence alone does not settle effects.
400 / title_required: The handler needs a nonblank trimmed title. A whitespace-only schema-valid string is refused; invalid local input is distinct from a server response.
Protection, database, Auth and shared runtime failures need their actual status/request evidence. Inserts/updates and final read-back are separate; a lost, invalid or missing read-back is not proof of no committed effect. Do not disable protection, replay credentials or probe another tenant to recover.
Related actions
Change log
- 2026-10-21: Added scoped lifecycle behavior, protection/concurrency boundaries, exact illustrative success and refusal bodies, side-effect-aware verification and honest compensation limits. All examples remain synthetic and unrun.