Topolo Docs Topolo Platform
Open workspace

Publish a form for public responses

Publish an approved existing definition while accounting for failure records, notification attempts and separate public-readiness checks.

Outcome

Publish an approved existing definition while accounting for failure records, notification attempts and separate public-readiness checks.

POST /api/forms/{formId}/publish

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.publish.

Requires formId, workspace:write and verified read_write workspace access, plus approval for public exposure and associated reference/notification effects. The selected route reads the scoped protected form first; a missing row returns404. Input carries no complete form body or version guard.

The selected publication check only requires at least one field outside section,divider,content,image,page_break. It is not a full content, privacy, consent, integration, translation, domain or legal readiness review. Approval must cover the actual current stored definition, which can change concurrently.

If that check fails, the handler inserts a publish-failure record, schedules a conditional Notify event to the form creator or current actor, and returns422 {error:form_not_publishable,reason:answer_field_required}. A refusal can therefore have durable and external side effects; do not treat422 as no-op or repeat it merely to test a notice.

On the normal path source reads the form again, updates status published and updatedAt, and keeps an existing publishedAt using COALESCE or sets the current time. It then reads back separately and schedules a forms.form.changed reference event with reason published. These reads/writes are not one version-checked transaction; repeated publishes can update timestamps and emit another transition event.

HTTP200 includes the full forms object plus action:{id:forms.publish,status,publicUrl}. publicUrl is null if the configured public base is absent or no id is returned; otherwise it is the configured base plus /public/<id>. A URL is not proof that an edge/domain, response submission, delivery integrations or notification has worked. The illustrative URL uses a reserved fictional domain.

Reference and failed-publication notification helpers require configured Notify/service identity and a subject/recipient. They catch emission failures; ctx.waitUntil is used when available. The form outcome, scheduled helper, provider receipt and actual recipient/public rendering remain separate.

Complete worked responses

First publication of an approved definition

Complete source-derived synthetic, unrun HTTP200. Assumes original authorized fictional form is draft with the shown single answer field, publishedAt:null, successful update/read-back, configured public base https://forms.example.test and shown clock. No form, provider, public URL or notification was exercised.

Input:

{
  "formId": "forms_docs_fixture"
}

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": "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-21T01:00:00.000Z",
    "submissionCount": 3
  },
  "action": {
    "id": "forms.publish",
    "status": "published",
    "publicUrl": "https://forms.example.test/public/forms_docs_fixture"
  }
}

Retain actual original target/request evidence and perform the verification/recovery below. The fixture is not a live outcome or authority.

Empty definition refused with possible side effects

Complete source-derived synthetic, unrun HTTP422. Assumes the current fictional form has no answer fields and the publish-failure insert succeeds. A failure record exists and a conditional notification helper is scheduled in this source path; no actual record or emission was produced here.

Input:

{
  "formId": "forms_docs_fixture"
}

Response:

{
  "error": "form_not_publishable",
  "reason": "answer_field_required"
}

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 scoped form lookup returns no row after original auth/workspace/write checks; no publish-failure record or publish update is reached.

Input:

{
  "formId": "forms_absent_docs_fixture"
}

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

  1. Read forms.get in the same original workspace and compare approved content, status, updatedAt and publishedAt. Preserve request evidence because the content can change between validation, update and read-back.

  2. When required, verify the approved public read and actual reference/notification state through supported readers separately. Do not create a respondent submission to prove publication. A nullable URL or failure response alone does not settle prior uncertain effects.

When separately approved, reconcile current content/effects and plan status:draft in the same workspace, then verify the stored state and required public propagation. This cannot retract prior submissions, failure records, reference events or notifications and is not a complete rollback.

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.

422 / form_not_publishable: Inspect reason answer_field_required and reconcile the already inserted failure record/conditional notification attempt. Correct actual approved content through a separate action; do not repeat unchanged publication as a harmless test.

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.

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.