# Form Publishing

> Form lifecycle management: draft, published, closed. Use when publishing forms, understanding public URLs, configuring captcha, or managing form branding.

- Skill: `builderio/form-publishing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add builderio/form-publishing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/builderio/form-publishing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Builder.io (https://skillmd.com/u/builderio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/builderio/form-publishing

---


# Form Publishing

## Form Lifecycle

Forms move through three statuses:

| Status      | Meaning                              | Public access |
| ----------- | ------------------------------------ | ------------- |
| `draft`     | Work in progress, not publicly available | No         |
| `published` | Live and accepting responses         | Yes           |
| `closed`    | No longer accepting responses        | Shows closed message |

## Publishing a Form

```bash
# Create as draft (default)
pnpm action create-form --title "Survey" --fields '[...]'

# Publish when ready
pnpm action update-form --id <form-id> --status published

# Close when done collecting responses
pnpm action update-form --id <form-id> --status closed
```

## Public URLs

Published forms are accessible at:

```
/f/<slug>
```

The slug is auto-generated from the title + a short unique suffix:
- Title: "Contact Form" -> Slug: `contact-form/a1b2c3`
- Full URL: `https://yourapp.com/f/contact-form/a1b2c3`

The slug updates automatically when the title changes.

## Captcha Protection

Public form submissions can be protected with Cloudflare Turnstile (opt-in). This prevents bot submissions without degrading the user experience.

## Branding

Public forms display a "Built with Agent-Native" badge by default. This can be configured in the form settings.

## Form Settings

Each form has a `settings` JSON object:

```json
{
  "submitText": "Submit",
  "successMessage": "Thank you! Your response has been recorded.",
  "redirectUrl": null,
  "completionMode": "message",
  "completionRefreshSeconds": 5,
  "showProgressBar": false,
  "emailOnNewResponses": false,
  "anonymous": false,
  "integrations": []
}
```

| Setting            | Type    | Description                                |
| ------------------ | ------- | ------------------------------------------ |
| `submitText`       | string  | Custom submit button text                  |
| `successMessage`   | string  | Message shown after successful submission  |
| `redirectUrl`      | string  | URL to redirect to after submission        |
| `completionMode`   | string  | `message`, `redirect`, `message_then_refresh`, or `refresh` |
| `completionRefreshSeconds` | number | Delay before refreshing for `message_then_refresh` (1–3600) |
| `showProgressBar`  | boolean | Show progress bar for multi-section forms  |
| `emailOnNewResponses` | boolean | Email the form owner's account when someone submits a response |
| `anonymous`        | boolean | Suppress IP, submitter identity, chat/run ids, page URL, and client-surface metadata for every response |
| `integrations`     | array   | Webhook/Slack/Discord/Google Sheets notification configs |

For a genuinely anonymous form, set `anonymous: true` when creating the form.
Do not describe an ordinary published form as anonymous: published forms accept
public responses, but only anonymous mode suppresses identifying and source
metadata.

Unset `completionMode` keeps legacy behavior: forms with a `redirectUrl` redirect
and all other forms show the success message. A redirect with an invalid URL
falls back to the success message.

## Integration Types

Forms can notify external services on submission:

| Type            | Description                     |
| --------------- | ------------------------------- |
| `webhook`       | POST JSON to any URL            |
| `slack`         | Send to a Slack channel through an Incoming Webhook URL |
| `discord`       | Send to a Discord webhook       |
| `google-sheets` | Send response JSON to a deployed Apps Script `/exec` URL |

These are outbound form destinations configured in the form builder's
**Integrations** tab. They are separate from the managed Slack/Messaging
connection. Google Sheets Apps Script handlers should parse
`JSON.parse(e.postData.contents)`; a spreadsheet URL or `/dev` URL will not
receive submissions.

## Related Skills

- **form-building** — Creating and structuring forms
- **form-responses** — Viewing data after forms are published

