[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Dynamic Form Builder

Build a Dynamic Form Builder

Build a dynamic form builder with drag-and-drop fields, conditional logic, multi-step flows, validation rules, submission handling, and analytics for no-code form creation.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 96/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
$

The Problem

Eva leads product at a 20-person HR tech company. Every client needs custom forms: onboarding surveys, performance reviews, exit interviews — each with different fields, validation, and logic. Currently, developers build each form in React. Adding a field takes a PR cycle. Conditional logic ("show section B only if answer A is 'Manager'") is hardcoded. They have 50 clients with 200 unique forms; each change request queues behind engineering. They need a form builder: drag-and-drop fields, conditional visibility, multi-step wizards, custom validation, and analytics — all configurable without code.

Step 1: Build the Form Engine

typescript
// src/forms/builder.ts — Dynamic form engine with conditional logic and multi-step support
import { pool } from "../db";
import { Redis } from "ioredis";
import { randomBytes } from "node:crypto";
import { z, ZodSchema } from "zod";

const redis = new Redis(process.env.REDIS_URL!);

interface FormDefinition {
  id: string;
  name: string;
  description: string;
  steps: FormStep[];
  settings: {
    submitButtonText: string;
    successMessage: string;
    redirectUrl?: string;
    notifyEmails: string[];
    allowMultipleSubmissions: boolean;
    requireAuth: boolean;
    expiresAt?: string;
  };
  status: "draft" | "published" | "archived";
  version: number;
  createdBy: string;
}

interface FormStep {
  id: string;
  title: string;
  description?: string;
  fields: FormField[];
  conditions?: StepCondition[];  // show step only if conditions met
}

interface FormField {
  id: string;
  type: "text" | "email" | "number" | "select" | "multiselect" | "textarea" | "date" | "file" | "rating" | "toggle" | "radio" | "checkbox" | "heading" | "paragraph";
  label: string;
  placeholder?: string;
  required: boolean;
  validation?: {
    minLength?: number;
    maxLength?: number;
    min?: number;
    max?: number;
    pattern?: string;
    customMessage?: string;
  };
  options?: Array<{ label: string; value: string }>;
  defaultValue?: any;
  conditions?: FieldCondition[];  // show field only if conditions met
  layout?: { width: "full" | "half" | "third"; order: number };
}

interface FieldCondition {
  fieldId: string;
  operator: "equals" | "not_equals" | "contains" | "gt" | "lt" | "is_empty" | "is_not_empty";
  value: any;
}

type StepCondition = FieldCondition;

// Create or update form definition
export async function saveForm(form: Omit<FormDefinition, "id" | "version">): Promise<FormDefinition> {
  const id = `form-${randomBytes(6).toString("hex")}`;
  const { rows: [existing] } = await pool.query(
    "SELECT MAX(version) as v FROM forms WHERE name = $1", [form.name]
  );
  const version = (existing?.v || 0) + 1;

  const full: FormDefinition = { ...form, id, version };

  await pool.query(
    `INSERT INTO forms (id, name, description, steps, settings, status, version, created_by, created_at)
     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW())`,
    [id, form.name, form.description, JSON.stringify(form.steps),
     JSON.stringify(form.settings), form.status, version, form.createdBy]
  );

  await redis.del(`form:${form.name}`);
  return full;
}

// Get published form for rendering
export async function getPublishedForm(formName: string): Promise<FormDefinition | null> {
  const cached = await redis.get(`form:${formName}`);
  if (cached) return JSON.parse(cached);

  const { rows: [row] } = await pool.query(
    "SELECT * FROM forms WHERE name = $1 AND status = 'published' ORDER BY version DESC LIMIT 1",
    [formName]
  );
  if (!row) return null;

  const form: FormDefinition = {
    ...row,
    steps: JSON.parse(row.steps),
    settings: JSON.parse(row.settings),
  };

  await redis.setex(`form:${formName}`, 300, JSON.stringify(form));
  return form;
}

// Evaluate field visibility based on conditions
export function evaluateConditions(
  conditions: FieldCondition[] | undefined,
  formData: Record<string, any>
): boolean {
  if (!conditions || conditions.length === 0) return true;

  return conditions.every((cond) => {
    const fieldValue = formData[cond.fieldId];
    switch (cond.operator) {
      case "equals": return fieldValue === cond.value;
      case "not_equals": return fieldValue !== cond.value;
      case "contains": return String(fieldValue || "").includes(cond.value);
      case "gt": return Number(fieldValue) > Number(cond.value);
      case "lt": return Number(fieldValue) < Number(cond.value);
      case "is_empty": return !fieldValue || fieldValue === "";
      case "is_not_empty": return !!fieldValue && fieldValue !== "";
      default: return true;
    }
  });
}

// Build Zod validation schema from form definition
export function buildValidationSchema(form: FormDefinition, currentData: Record<string, any>): ZodSchema {
  const shape: Record<string, any> = {};

  for (const step of form.steps) {
    if (!evaluateConditions(step.conditions, currentData)) continue;

    for (const field of step.fields) {
      if (field.type === "heading" || field.type === "paragraph") continue;
      if (!evaluateConditions(field.conditions, currentData)) continue;

      let schema: any;
      switch (field.type) {
        case "email": schema = z.string().email(field.validation?.customMessage || "Invalid email"); break;
        case "number": case "rating":
          schema = z.number();
          if (field.validation?.min !== undefined) schema = schema.min(field.validation.min);
          if (field.validation?.max !== undefined) schema = schema.max(field.validation.max);
          break;
        case "multiselect": case "checkbox":
          schema = z.array(z.string()); break;
        case "toggle":
          schema = z.boolean(); break;
        case "date":
          schema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/); break;
        default:
          schema = z.string();
          if (field.validation?.minLength) schema = schema.min(field.validation.minLength);
          if (field.validation?.maxLength) schema = schema.max(field.validation.maxLength);
          if (field.validation?.pattern) schema = schema.regex(new RegExp(field.validation.pattern));
      }

      shape[field.id] = field.required ? schema : schema.optional();
    }
  }

  return z.object(shape);
}

// Submit form response
export async function submitForm(
  formName: string,
  data: Record<string, any>,
  context: { userId?: string; ip: string; userAgent: string }
): Promise<{ id: string; valid: boolean; errors?: Record<string, string> }> {
  const form = await getPublishedForm(formName);
  if (!form) throw new Error("Form not found");

  // Check expiry
  if (form.settings.expiresAt && new Date(form.settings.expiresAt) < new Date()) {
    throw new Error("Form has expired");
  }

  // Validate
  const schema = buildValidationSchema(form, data);
  const result = schema.safeParse(data);

  if (!result.success) {
    const errors: Record<string, string> = {};
    for (const issue of result.error.issues) {
      errors[issue.path.join(".")] = issue.message;
    }
    return { id: "", valid: false, errors };
  }

  const id = `sub-${randomBytes(8).toString("hex")}`;

  await pool.query(
    `INSERT INTO form_submissions (id, form_id, form_version, data, user_id, ip, user_agent, created_at)
     VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())`,
    [id, form.id, form.version, JSON.stringify(data), context.userId, context.ip, context.userAgent]
  );

  // Notify
  if (form.settings.notifyEmails.length > 0) {
    await redis.rpush("notification:queue", JSON.stringify({
      type: "form_submission", formName, submissionId: id,
      emails: form.settings.notifyEmails,
    }));
  }

  // Analytics
  await redis.hincrby(`form:analytics:${form.id}`, "submissions", 1);
  await redis.hincrby(`form:analytics:${form.id}`, `step_completions`, form.steps.length);

  return { id, valid: true };
}

// Form analytics
export async function getFormAnalytics(formId: string): Promise<{
  totalSubmissions: number;
  completionRate: number;
  avgCompletionTime: number;
  fieldDropoffs: Record<string, number>;
}> {
  const stats = await redis.hgetall(`form:analytics:${formId}`);
  const { rows: [{ count }] } = await pool.query(
    "SELECT COUNT(*) as count FROM form_submissions WHERE form_id = $1", [formId]
  );

  return {
    totalSubmissions: parseInt(count),
    completionRate: 0,
    avgCompletionTime: 0,
    fieldDropoffs: {},
  };
}

Results

  • Form creation: 2 weeks → 30 minutes — HR team drags fields, sets conditions, publishes; no engineering PR needed; 200 forms managed by non-technical staff
  • Conditional logic without code — "show 'Manager Name' field only when Role = 'Individual Contributor'" configured in the builder; reduces form length by 40% for most users
  • Multi-step wizards — performance review split into 5 steps with progress bar; completion rate up 25% vs single long form
  • Real-time validation — Zod schema generated dynamically from form definition; errors shown inline; invalid submissions dropped from 15% to 2%
  • Submission analytics — dashboard shows step-by-step dropoff; Step 3 had 30% dropoff → simplified from 8 fields to 4 → dropoff dropped to 8%

Related use cases