agentleFS
buildpad-aibuildpad-ai/skills/create-form-builder/SKILL.md

create-form-builder

Skill1 starsChanged 27 days agoMITView on GitHub

Scaffold and use the Buildpad Dynamic Form Builder — a visual drag-and-drop authoring UI (FormBuilder) plus a runtime renderer (DynamicForm) layered on CollectionForm/VForm. An admin arranges a collection's fields into sections, sets width/required/readonly/hidden, and authors conditional logic, saving a reusable FormDefinition into an fb_definitions collection (the schema is never mutated). Use when the user says form builder, build a form, dynamic form, form designer, visual form editor, conditional form, or wants end users to design forms.

  • Reads credentials

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Read .buildpad/project.json at the project root.

    line 14
  2. Very specific:

    If a folder named supabase at the project root holds a file named config.toml, or holds a folder named migrations that holds a file whose …

    line 19

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Create with the Dynamic Form Builder

<!-- backend-gate:begin v1 -->

Establish the backend first. A Buildpad project's backend is either the Buildpad Engine or Supabase, and some parts of this file hold for one of them only. Before you act on anything in this file, find the backend with these steps, in order, and stop at the first step that answers:

  1. Read .buildpad/project.json at the project root. If it is JSON and its field backend is "engine" or "supabase", that is the backend. If .buildpad/backend-answer.json names the other backend, tell the developer the two files disagree, and follow .buildpad/project.json.
  2. Otherwise read .buildpad/backend-answer.json in the same way; if its backend is "engine" or "supabase", that is the backend.
  3. If a folder named supabase at the project root holds a file named config.toml, or holds a folder named migrations that holds a file whose name ends in .sql, the backend is Supabase.
  4. Otherwise ask the developer once: "Is this project's backend the Buildpad Engine or Supabase?" Write the answer to .buildpad/backend-answer.json as {"buildpadBackendAnswer": 1, "backend": "engine"} or {"buildpadBackendAnswer": 1, "backend": "supabase"}, and ask the developer to commit that file. Do not add it to .gitignore. Never write .buildpad/project.json.

To find the backend, do not open an environment file (.env, .env.*) or an MCP configuration file, and do not call a tool that returns project credentials.

Then follow only the parts for that backend. A part starts at a line that holds only a comment whose text is buildpad + -backend: engine or buildpad + -backend: supabase, and ends at the next line that holds only a comment whose text is /buildpad + -backend. A comment that merely mentions a backend is not a part. A file whose first line, after any front matter, holds only a comment whose text is buildpad + -backend-file: engine (or supabase) holds for that backend only, and so does every file in a folder named engine-only or supabase-only. Skip the other backend's parts and copy no code from them; everything else holds for both. On an Engine project, when this file says that something is not available, tell the developer so and stop: do not build a substitute.

<!-- backend-gate:end --><!-- buildpad-backend: engine -->

On an Engine project with Customer_Tenants each customer of the application is a tenant of its own, and the project's tenant is the Blueprint every customer is copied from. Establish the tenancy mode as the add-multitenancy skill says: from the field tenancy of .buildpad/project.json, else from .buildpad/tenancy-answer.json. In the mode customer-tenants that skill's guide engine-customer-tenants replaces what ES-3, ES-4, ES-5, ES-7 and ES-9 say about the project's tenant; in the mode scope they hold as written.

<!-- /buildpad-backend -->

A ready-made /forms experience: a visual form builder plus a runtime renderer on top of the schema-driven CollectionForm/VForm runtime. An admin arranges a collection's fields into ordered sections, sets width / required / readonly / hidden, and authors conditional logic, then saves the result as a reusable, named form definition. Definitions are stored as items in an ordinary collection and merged onto the live schema at render time via buildFieldsFromDefinition — the schema is never mutated.

This is a two-step workflow: author a definition with FormBuilder, then fill it with DynamicForm. That is why this is a create-* workflow skill, not just a component install.

Relationship to VForm (do not confuse)

  • @buildpad/ui-forms (this — plural) = the Form Builder module.
  • @buildpad/ui-form (singular) = the low-level VForm runtime the builder wraps.

The builder emits fields consumed by CollectionForm → VForm. Do not re-implement VForm's controlled-component pattern here — the runtime handles state and submission. See vform-usage for the substrate.

Prerequisite: the definitions collection

Definitions are persisted as items in a single collection (default fb_definitions, overridable via formsCollection). It is an ordinary user collection — not a daas_-prefixed system collection — and must exist before the builder can save:

fieldtypenotes
iduuid (PK)auto
namestringform name
target_collectionstringcollection the form targets
keystring (nullable)optional discriminator so multiple forms can share one collection (e.g. per issue type)
definitionjsonthe FormDefinition body

If the collection is missing, the list and builder render a FormsEmptyState hint instead of failing. With DaaS schema rights that hint offers a one-click Create collection action. Use create-collection to provision it explicitly if you prefer, on either kind of project; on a Supabase project create-migration also works (an Engine project has no migrations).

Authoring (arranging existing fields) needs only item create/update/read on the definitions collection plus read on the target collection's schema — not admin/DDL rights. Only provisioning (creating a collection or a new real column) needs schema rights.

Installation

The Forms module is opt-in — it is not part of bootstrap.

# 1. Add the builder + renderer + /forms pages + useFormDefinitions hook
npx @buildpad/cli@latest add form-builder forms-routes --cwd /path/to/project

# 2. Add the DaaS proxy routes it needs (if not already present)
npx @buildpad/cli@latest add api-routes --cwd /path/to/project

The docs use the shorthand buildpad add form-builder forms-routes; the npx @buildpad/cli@latest add … form is the equivalent no-install invocation used across these skills.

If the CLI command fails, @buildpad/cli IS published to npm — verify with npm view @buildpad/cli version before assuming otherwise or reaching for a local clone. The CLI fetches its registry from raw.githubusercontent.com, so confirm the environment can reach both npm and the GitHub raw CDN. A local clone is only needed for CLI development, never to consume components.

The module must render under the authenticated layout (app/(authenticated)/ with a DaaSProviderWrapper) so DaaSProvider header injection (the signed-in user's session token) is in scope — the hooks call /api/items/* (and, when provisioning in proxy mode, /api/fields/* and /api/collections/*).

On a Supabase project, DaaS CORS must be configured first. DynamicForm (via CollectionForm) fetches /users/me, /permissions/me, and item endpoints from the browser with credentials: 'include'. DaaS's default cors_origins: ["*"] is incompatible with credentialed requests — the browser blocks every preflight (Access-Control-Allow-Origin header must not be the wildcard '*'). Set explicit origins first (see daas-platform "CORS must use explicit origins" — Bugs 17+25 — and debugging-and-error-recovery). This is not form-builder-specific: it affects every authenticated Buildpad component.

On an Engine project there is nothing to configure. On an Engine project the data API accepts requests from any origin and never with cookies, as the project is provisioned. Change no cross-origin setting, and do not add the application's origin anywhere.

The Routes

app/forms/
├── page.tsx            # /forms          — list of saved definitions
├── new/page.tsx        # /forms/new      — create (auto-create or bind existing)
├── [id]/page.tsx       # /forms/[id]     — the FormBuilder editor
└── [id]/fill/page.tsx  # /forms/[id]/fill — render with DynamicForm to create an item

Step 1 — Author with FormBuilder

// app/forms/new/page.tsx
import { FormBuilder } from "@/components/ui/form-builder";

// Start from scratch — auto-create a full fb_ collection on save (needs schema rights):
<FormBuilder onSaved={(def) => router.push(`/forms/${def.id}`)} />

// Bind to an existing collection (hybrid storage):
<FormBuilder targetCollection="issues" onSaved={(def) => router.push(`/forms/${def.id}`)} />
// app/forms/[id]/page.tsx — edit an existing definition (target comes from the definition)
<FormBuilder definitionId={id} />
PropTypePurpose
targetCollectionstringRequired when creating a bound form; omit when editing or "start from scratch"
definitionIdstring | numberEdit an existing definition
formsCollectionstring (default fb_definitions)Definitions collection name
onSaved(def: FormDefinition) => voidCalled after a successful save with the persisted definition

The Build tab is a three-pane workspace — field-type palette (+ Add extra field + existing collection fields) on the left, the section canvas in the middle, a per-field settings panel (width / required / hidden / conditions) on the right — plus a Preview tab that renders the in-progress form live.

Step 2 — Fill with DynamicForm

// app/forms/[id]/fill/page.tsx
"use client";

import { use } from "react";
import { useRouter } from "next/navigation";
import { DynamicForm } from "@/components/ui/form-builder";

export default function FillFormPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = use(params);
  const router = useRouter();
  return (
    <DynamicForm
      definitionId={id}
      onSuccess={() => router.push("/forms")}
      onCancel={() => router.push("/forms")}
    />
  );
}
PropTypeDefaultPurpose
definitionIdstring | number— (required)The saved definition to render
formsCollectionstringfb_definitionsDefinitions collection name
itemIdstring | number—Provide to switch the form to edit mode
onSuccess(data?) => void—Called after a successful create/update
onCancel() => void—Called on cancel
scopeFieldstring'resource_uri'Target-collection field holding the scope URI (multi-tenant)
injectActiveScopebooleantrueInject the active scope URI as a create-time default

The List View & Empty State

// app/forms/page.tsx
import { FormsEmptyState, type FormDefinition } from "@/components/ui/form-builder";
import { useFormDefinitions } from "@/lib/buildpad/hooks";

const { list } = useFormDefinitions();
// list().then(setDefinitions).catch(...) — render FormsEmptyState on error/missing collection

Data Model & Hook (quick reference)

  • Hook useFormDefinitions() from @/lib/buildpad/hooks → list, get, create, update, remove, resolveScreen (the API keeps the legacy screen name for a resolved definition), plus loading/error. DEFAULT_FORMS_COLLECTION = 'fb_definitions'.
  • On an Engine project a list request returns at most 1000 items (limit is capped at 1000), and remove is answered 204 No Content with no body.
  • Types FormDefinition, FormSection, FormFieldConfig are re-exported from the component barrel @/components/ui/form-builder (as used in the list-view example above). FieldCondition is the exception — it is deliberately not re-exported from the barrel, so import it from @/lib/buildpad/types.

For the full data model, conditions cookbook, storage strategies, and multi-tenant scope behaviour, read references/form-builder.instructions.md.

See It Live

Forms Storybook (port 6010): pnpm --filter @buildpad/ui-forms storybook. The FormBuilder (DaaS) story connects to a real DaaS instance via the storybook-host proxy.

Related

  • create-collection — provision the fb_definitions or target collections (on either kind of project).
  • create-feature/vform-usage — the VForm runtime the builder wraps.
  • buildpad-reference — full component catalog.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.