---
title: "Form Builder Plugin"
aliases: [form-builder, plugin-form-builder, payloadcms-forms]
tags: [payloadcms, plugin, forms, email, payments, uploads]
sources: [raw/plugins__form-builder.md]
created: 2026-05-15
updated: 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

```bash
pnpm add @payloadcms/plugin-form-builder
```

```ts
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.

```ts
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

```ts
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`):

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

## Upload Fields

Disabled by default. Enable and configure upload collections:

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

**Frontend submission via `multipart/form-data`:**

```ts
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

```ts
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|email configuration]] (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:

```ts
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

```ts
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)

## Related

- [[wiki/payloadcms/plugins|Official Plugins]] — all 10 plugins overview
- [[wiki/payloadcms/email|Email — Adapters]] — Nodemailer, Resend, attachments
- [[wiki/payloadcms/upload|Upload & Media]] — upload collections, storage adapters
- [[wiki/payloadcms/hooks-collections|Collection Hooks]] — beforeChange, afterChange patterns
- [[wiki/payloadcms/authentication-overview|Authentication Overview]] — access control on collections

## Sources

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