obsidian/wiki/payloadcms/plugin-form-builder.md
2026-05-15 16:16:30 +01:00

7.1 KiB

title aliases tags sources created updated
Form Builder Plugin
form-builder
plugin-form-builder
payloadcms-forms
payloadcms
plugin
forms
email
payments
uploads
raw/plugins__form-builder.md
2026-05-15 2026-05-15

Overview

@payloadcms/plugin-form-builder lets admins build and manage forms dynamically from the Admin Panel — no hard-coding new forms in the codebase. The front-end maps over the schema and renders its own UI components. All submissions are stored in the DB; confirmations can be on-screen messages or redirects; dynamic emails are sent on submission.

Replaces third-party services (HubSpot, Mailchimp) with first-party tooling.

Installation

pnpm add @payloadcms/plugin-form-builder
import { formBuilderPlugin } from '@payloadcms/plugin-form-builder'

export default buildConfig({
  plugins: [formBuilderPlugin({ /* options */ })],
})

Key Options

Option Type Purpose
fields object Enable/disable/override individual field types
redirectRelationships string[] Collection slugs available as redirect targets
beforeEmail hook Transform emails just before they are sent
defaultToEmail string Fallback recipient if form config omits to
formOverrides CollectionConfig Override the forms collection (slug, access, fields)
formSubmissionOverrides CollectionConfig Override the form-submissions collection
handlePayment hook Process payment on submission (use with payment field)
uploadCollections string[] Required when upload field is enabled

Security Warning

The forms collection is publicly readable by default — including the emails field. Override formOverrides.access to restrict it, especially if you have frontend users.

formBuilderPlugin({
  formOverrides: {
    access: {
      read: ({ req: { user } }) => !!user,
    },
    fields: ({ defaultFields }) => [...defaultFields, { name: 'custom', type: 'text' }],
  },
})

Available Field Types

Field Maps to Notes
text <input type="text"> Simple string
textarea <textarea> Multiline string
select <select> Options array with label/value
radio radio group Options array with label/value
email <input type="email"> Email with format validation
state <select> US states
country <select> Countries
checkbox <input type="checkbox"> Boolean
date <input type="date"> UTC stored; timezone configurable
number <input type="number"> Numeric
message RichText Display-only content block in form
payment Triggers handlePayment on submit; disabled by default
upload file input Disabled by default; requires uploadCollections config

All fields share common properties: name, label, defaultValue, width, required.

Field Overrides

import { fields } from '@payloadcms/plugin-form-builder'

formBuilderPlugin({
  fields: {
    text: {
      ...fields.text,
      labels: { singular: 'Custom Text Field', plural: 'Custom Text Fields' },
    },
    date: false, // disable
  },
})

GraphQL Name Collision Fix

If your own blocks/collections use the same names as plugin fields (e.g. Country):

formBuilderPlugin({
  fields: {
    country: { interfaceName: 'CountryFormBlock' },
  },
})

Upload Fields

Disabled by default. Enable and configure upload collections:

formBuilderPlugin({
  fields: {
    upload: {
      uploadCollections: ['media', 'documents'], // required
    },
  },
})

Frontend submission via multipart/form-data:

const formData = new FormData()
formData.append(field.name, fileValue) // upload fields
formData.append('_payload', JSON.stringify({ form: formId, submissionData }))

await fetch('/api/form-submissions', { method: 'POST', body: formData })

The server validates MIME types/sizes, uploads to the collection, and stores file IDs.

Alternative: pre-upload then submit ID — upload file first via POST /api/{collection}, then pass the returned doc.id as the field value.

Presigned URL flow (for large files with S3/GCS/Azure clientUploads enabled):

  1. POST /api/storage-s3-generate-signed-url → get presigned URL
  2. PUT file directly to cloud storage
  3. POST /api/{collection} with metadata to create the DB document

Payment Integration

import { getPaymentTotal } from '@payloadcms/plugin-form-builder'

formBuilderPlugin({
  handlePayment: async ({ form, submissionData }) => {
    const paymentField = form.fields?.find(f => f.blockType === 'payment')
    const price = getPaymentTotal({
      basePrice: paymentField.basePrice,
      priceConditions: paymentField.priceConditions,
      fieldValues: submissionData,
    })
    // call your payment processor here
  },
})

priceConditions define conditional pricing rules (field → condition → operator → value → delta).

Email

Uses Payload's wiki/payloadcms/email (Nodemailer/Resend). Supports:

  • Dynamic field interpolation: Thank you, {{name}}!
  • {{*}} wildcard — outputs all fields as key: value
  • {{*:table}} — outputs all fields as HTML table
  • Rich text body serialized to HTML server-side

beforeEmail hook — inject HTML templates before sending:

beforeEmail: (emailsToSend) =>
  emailsToSend.map(email => ({ ...email, html: wrapTemplate(email.html) }))

SendGrid gotcha: if using Link Branding + Domain Authentication, the from address must be on your verified domain. from: {{email}} will fail — use website@your_domain.com instead.

TypeScript

import type {
  PluginConfig, Form, FormSubmission,
  FieldsConfig, BeforeEmail, HandlePayment,
  UploadField, UploadFieldMimeType,
} from '@payloadcms/plugin-form-builder/types'

Key Takeaways

  • Install @payloadcms/plugin-form-builder, add to plugins[] — creates forms and form-submissions collections automatically
  • forms collection is world-readable by default — override formOverrides.access in production
  • Upload field is disabled by default — enable it and set uploadCollections
  • Payment field calls handlePayment hook; use getPaymentTotal() for conditional pricing
  • Email uses Payload's email config with {{fieldName}} interpolation; SendGrid domain auth restricts from address
  • GraphQL type name collisions from plugin fields → fix with interfaceName override
  • form-submissions allows public create but blocks update and read by default (correct for public forms)

Sources

  • raw/plugins__form-builder.md — official Payload CMS Form Builder Plugin docs