Forms & Form Builder

Create forms with the visual Form Builder and connect them to custom blocks by storing the form's ID in a block field.

March 31, 2026

Overview

Cmssy has a built-in Form Builder that lets you create forms visually and connect them to any custom block. Forms handle validation, submissions, email notifications, and webhooks — so your blocks only need to render the UI.

The system has two parts:

  1. Form Builder (Dashboard > Forms) — define fields, validation, and actions
  2. A formId field on your block — stores which form the block should render

Creating a Form

1. Open the Form Builder

Go to Dashboard → Forms → Create Form. Give it a name and slug (e.g. contact-form).

2. Add Fields

Each field has:

PropertyDescription
nameUnique field key (e.g. email, message)
typetext, email, textarea, number, phone, url, date, select, multiselect, checkbox, radio, file, hidden
labelDisplay label (multilingual)
placeholderPlaceholder text (multilingual)
validationrequired, minLength, maxLength, minValue, maxValue, pattern, customMessage
widthfull, half, or third — controls field width in the form grid
optionsFor select, multiselect, radio — array of { value, label }
orderDisplay order

3. Configure Settings

SettingDescription
Action Typecontact (email + save), newsletter (subscribe), login, register, custom (webhook)
Email RecipientsList of emails that receive form notifications
Webhook URLFor custom action — POST submission data to an external endpoint
Success MessageShown after successful submission (multilingual)
Error MessageShown on failure (multilingual)
Submit Button LabelButton text (multilingual)
Save SubmissionsStore submissions in the database (default: true)
Send Email NotificationEmail recipients on each submission (default: true)
Enable CaptchaSpam protection
Require LoginOnly logged-in users can submit

4. Publish

Set status to Published. Copy its form ID — you'll reference it from your block.

Using Forms in Custom Blocks

Referencing a Form

Add a formId field to your block schema. Use fields.form(): it renders a form picker in the editor and stores the chosen form's id, so nobody has to copy an id by hand.

// blocks/contact/block.ts
import { defineBlock, fields } from "@cmssy/react";
import Contact from "./Contact";

export const contactBlock = defineBlock({
  type: "contact",
  label: "Contact",
  component: Contact,
  props: {
    formId: fields.form({ label: "Form" }),
    submitLoadingText: fields.text({ label: "Loading Text", defaultValue: "Sending..." }),
    successHeading: fields.text({ label: "Success Heading", defaultValue: "Message Sent!" }),
  },
});

Rendering the Form

The SDK automatically resolves any form referenced by a block's formId and injects the definition into the block context as context.forms[formId] — no fetching needed:

// blocks/contact/src/Contact.tsx
export default function Contact({ content, context }) {
  const { formId } = content;
  const formDef = formId ? context?.forms?.[formId] ?? null : null;
  // formDef.fields   - array of field definitions
  // formDef.settings - submit label, messages, action type
  // ...render the fields
}

Submitting the Form

Submitting goes through public.form.submit. Keep the mutation in your own repo and send it with your configured client - the SDK carries the same string as SUBMIT_FORM_MUTATION, but under @cmssy/core/internal, which is not public API. The backend validates the fields, saves the submission, sends the notification emails, calls any webhook, and answers with a success/error response:

// blocks/contact/actions.ts
"use server";
import { createCmssyClient, type CmssyFormSubmitResponse } from "@cmssy/react";
import { cmssy } from "@/cmssy.config";

const SUBMIT_FORM = `mutation SubmitForm($formId: ID!, $input: SubmitFormInput!) {
  public {
    form {
      submit(formId: $formId, input: $input) {
        success
        message
        submissionId
        redirectUrl
      }
    }
  }
}`;

const client = createCmssyClient(cmssy);

export async function submitForm(formId: string, data: Record<string, string>) {
  const res = await client.query<{
    public: { form: { submit: CmssyFormSubmitResponse } };
  }>(SUBMIT_FORM, { formId, input: { data } });

  return res.public.form.submit; // { success, message, submissionId, redirectUrl }
}

Submission is the one write a public frontend can make without a token. Everything else about forms - creating them, reading submissions, changing status - needs an authorized client.

Reference Form Blocks

Cmssy doesn't ship blocks — in the headless model you build blocks in your own repo with the SDK. These are common form blocks you can implement as reference patterns, each wiring a Form Builder form (its actionType) to a block you render:

BlockForm ActionDescription
ContactcontactContact form with info cards and quote
NewsletternewsletterEmail signup form
LoginloginAuthentication form
RegisterregisterUser registration
Forgot PasswordloginPassword reset request

Each defines the form server-side in the Form Builder and renders it in a block via a formId field — the dev owns the markup. See Member Authentication for the auth flows.

Form Submissions

Viewing Submissions

Go to Dashboard → Forms → [Your Form] → Submissions. Each submission shows:

  • All field values
  • Status (pending, processed, spam, archived)
  • IP address, user agent, referrer
  • Timestamp
  • Email/webhook delivery status

Submission Lifecycle

  1. User submits form on your published site
  2. Server validates fields and checks rate limits
  3. Submission is saved with status pending
  4. Email notification sent to recipients (if enabled)
  5. Webhook called (if configured)
  6. Status updated to processed

Spam Protection

Forms include built-in rate limiting:

  • Per-IP rate limit per form
  • Optional captcha support
  • Honeypot fields can be added as hidden field type

Action Types Reference

contact

Saves submission + sends email to configured recipients. The default for most forms.

newsletter

Subscribes the email field to the workspace newsletter list.

login / register

Handles authentication. These are special action types used by the auth form blocks you build (see Member Authentication).

custom

POSTs the submission data as JSON to your webhook URL. Useful for integrating with external services (Zapier, Slack, CRMs, etc.).

Tips

  • Always publish your form before referencing it — draft forms can't be resolved
  • Use half and third widths to create multi-column form layouts (e.g. first name + last name side by side)
  • Multilingual labels — form fields support per-language labels and placeholders, matching the site's language system
  • Test submissions are saved like real ones — delete them from the Submissions tab when done
  • Webhook debugging — check the submission's webhookResponse field for response details from your endpoint