TL;DR for Agents
/builder-ui; default port 52002; backend must be running.pnpm install first. Bug fixes start with pnpm test:no-coverage; lint with pnpm test:lint.app/src/app.css only for global design system primitives (buttons, inputs, typography); run pnpm build:css if needed; never touch app/src/app.dist.css.app/src/icons/index.tsx, app/src/app.dist.css, app/src/gql/schema.json.Audience: AI agents only. Assumes you can run commands, read code, and follow guardrails.
The specs/ directory is a symlink to ../specs (a separate repository). If the symlink is broken or the specs repo is not available, continue without specs access.
specs/archive/ - Archived specifications (if available). NEVER read, reference, or use these files.Before starting work:
pnpm install/builder-ui| Task Type | Primary Tools | Key Sections | First Commands |
|---|---|---|---|
| Bug fix | Read, Edit, Bash | Debugging Playbook, Architecture | pnpm test:no-coverage |
| New feature | Task (Explore), TodoWrite, Edit | Core Principles, Task Recipes | Explore codebase first |
| Code review | Task (code-reviewer) | Testing Guide, Core Principles | Review changes |
| Refactoring | Read, Edit, Task | Architecture, Core Principles | Understand structure first |
| Testing | Read, Write | Testing Comprehensive Guide | pnpm test:no-coverage |
| Styling | Edit, Bash | Frontend Conventions (Styling) | Tailwind for components; app.css for globals only |
| i18n/Translation | Edit, Bash | Frontend Conventions (i18n) | pnpm extract |
@ → app/src@testing/* → app/src/@testing/*app/src/index.jsx mounts router from app/src/routes.jsxapp/src/root-layout.jsx (Apollo provider, theme, alerts, feature-flag modal, SystemError boundary, keyboard shortcuts)app/src/config.jsxapp/src/app.css (source) → app/src/app.dist.css (compiled, never edit directly)app/src/routes.jsx - React Router 7 route treeapp/src/gql/index.jsx (Apollo Client with custom type policies)app/src/gql/schema.json (generated)app/src/i18n.jsx/app/api/v0/graphqlauthToken cookieapp/src/gql/index.jsx (the typePolicies export, ~lines 27-172)/api/v1/* for Identity Service (user management)Frontend (React)↓ GraphQL Query/MutationPlatform (Elixir/Phoenix, `/app/api/v0/graphql`)├─→ MongoDB (tenant/customer data, basic integrations)├─→ Postgres (Oban jobs, Bridge/Lasso integrations, internal data)├─→ Identity Service (Node.js/Express, `/api/v1/users/*`) ⇄ MongoDB (user auth data)├─→ Forms API (Node.js/Express) - form processing/validation├─→ Workflows API (Node.js/Express) - workflow execution└─→ PDF API - PDF generationIdentity Service can call Platform back for user extended attributes (bi-directional)
app/src/pages-builder/ - Builder/configuration interface (form designer, workflow, dashboard, permissions, publishing)
app/src/pages-runner/ - End-user runtime (run, edit, view forms, workflow actions, document history)
app/src/pages/ - System administration (home, identity management, integrations, audit, spaces, settings, usage, permissions)
app/src/pages-anonymous/ - Public form submission (unauthenticated, separate Apollo client)
app/src/formbot/ - Form rendering engine
index.jsx - Main formbot instance, gadget registration, validationgadgets/ - Many gadget types including Text, Dropdown, Repeater, Table, DataLookup, and moreengine/ - Core rendering and state managementdecorators/ - Validation, progressive disclosure, runtime enhancementsapp/src/flowbot/ - Visual workflow designer and execution engine
engine/ - Visual editor, viewer, configuration panel, validation, simulationsteps/ - 9 step types: approval, task, notification, formfill, acknowledge, conditional, integration, echo, triggercomponents/ - UI components (email builder, person picker, etc.)app/src/voronoi-dnd/ - Generic drag-and-drop library
voronoi.jsxdraggable.jsx, drop-zone.jsx, gatherer.jsx, item.jsx, context.jsxapp/src/voronoi-dnd-formbot/ - Form-specific DND implementation
app/src/components/ - Shared UI components: layouts, modals (modal-centered.jsx, modal-page.jsx), data tables, identity pickers, feature flags, error boundaries, spinners
app/src/ui/ - Complex system primitives and utilities: alerts, popovers, tabs, tooltips, lookup, theme utilities, a11y helpers, and shadcn components (dropdown-menu, sheet, sidebar, skeleton)
app/src/icons/ - Auto-generated SVG icons (never edit index.tsx manually, use import scripts)
app/src/illustrations/ - Decorative SVGs for empty states and success screens
app/src/gql/ - GraphQL config: Apollo Client setup (index.jsx), schema (schema.json), type policies
app/src/@testing/ - Test utilities: Mockley, test helpers, fixtures. Setup in .test/setup.js
pages-builder/pages-runner/pages/components/ui/formbot/ with many gadget typesvoronoi-dnd/voronoi-dnd-formbot/flowbot/ - custom-built linear workflow editor with drag-and-dropuseOutletContext exists from React Router 7 migration; prefer Context/Apollo for new featuresCritical environment variables used throughout the application:
Configuration lives in app/src/config.jsx:
window.loggedInUser used throughout for user contextLocation: app/src/gql/index.jsx (the typePolicies export, ~lines 27-172)
Why they exist: Workarounds for caching issues caused by API design/query patterns. Ideally would be addressed at API level.
Existing policies (understand these when reading code, avoid adding new ones if possible):
__refformVersion.id to cache different versions separatelykeyFields: false) - too dynamic to cacheIf experiencing cache issues: First investigate if query/API can be fixed. Only add type policy as last resort.
Error Boundary:
SystemError component (app/src/components/system-error.jsx)/auth, permission errors redirect to Forbidden componentSentry Integration:
app/src/config.jsx with custom breadcrumb/error contextVITE_SENTRY_RELEASE is setGraphQL Errors:
/auth?return_to=<current>Forbidden componentExplicitError or GraphQLError components for manual error displayAdding new external domains:
scripts/generate-csp/index.jsscript-src, connect-src, img-src)node scripts/generate-csp to verifyCSP includes third-party integrations (AnnounceKit, ChurnZero, Sentry), allows 'unsafe-inline', WebSocket across Kuali domains, S3 image buckets.
Use app/src/components/sanitize.jsx for sanitizing user-generated HTML in rich text gadgets and email content.
Pre-commit runs Prettier + ESLint on staged files (configure via git config hooks.validatebuilderui on|off|custom). Bypass with --no-verify.
Config: eslint.config.mjs (ESLint 9 flat config)
Prioritize thoroughness over speed. Correctness and consistency matter more than quick completion. Always read existing code, understand patterns, write tests, and verify changes work properly. Taking time to do it right prevents technical debt.
Always read code before modifying it. Never propose changes to files you haven't read. Before adding or modifying code:
Example: If modifying a GraphQL mutation file and all existing mutations use mapValues(keyBy(...)) for error handling, use that same pattern - don't introduce a new approach.
Check MDN documentation - arrays have .includes(), not .contains()
Use clear, semantic field names (type, parts). Don't overload fields for multiple purposes.
When handling nested structures, check for nested patterns first before handling leaf nodes.
Test fixtures must use exact same structure as production code with all required fields.
Remove incomplete/invalid data before processing - validate at boundaries.
Support both old and new patterns during transitions. Transform at edges, not throughout.
Always validate data existence. Check arrays/objects exist before operations. Return early with sensible defaults.
Avoid premature abstraction. Compose small components. Separate business logic from presentation.
Only make requested changes. Keep solutions simple. Don't add features beyond requirements. Three similar lines > premature abstraction.
Test what code does, not how. Focus on user-visible behavior. Cover happy path + edge cases. Avoid testing internals.
Only use useEffect for external system synchronization (WebSockets, DOM manipulation, third-party libraries, analytics).
Never use useEffect for:
Always include cleanup functions to prevent memory leaks.
Only change what's necessary for your task. Don't rename variables, reformat code, fix typos, add/remove whitespace, or refactor unrelated logic. Keep diffs focused on the actual functional change.
However, when your change makes something obsolete:
.filter(Boolean) on non-conditional arrays)These are not "extra" changes—they're completing the work. A variable that's assigned-but-never-used after your change should be removed as part of the same change. Every line touched increases review burden and merge conflict risk, but leaving dead code is worse.
When a variable is simply assigned once and never reassigned, use the original value directly instead of creating an intermediate variable.
Bad:
const gadgets = anonymousDisabledGadgetsif (gadgets.has(gadget.key)) return true
Good:
if (anonymousDisabledGadgets.has(gadget.key)) return true
Exception: Keep intermediate variables when they:
Before considering work complete, review your changes for:
.filter(Boolean) when arrays no longer have conditional elements?This self-review catches issues that PR reviewers would otherwise need to point out.
pnpm test:watch) in automated fix loops - use pnpm test:no-coverage for single runs instead.pnpm test:no-coverage sequentially, not in parallel with other resource-intensive commands./auth; uses window.loggedInUser or fetches from /api/v1/users/current; token in authToken cookiessoId field (SAML/CAS/LDAP). Cannot set/change passwords (HTTP 400)passwordDigest (bcrypt, 10 rounds). Backend never returns password; UI enforces complexity rulesuser.ssoId per-userUse CenterModal (app/src/components/modal-centered.jsx):
role="dialog", aria-modal="true", focus trap, ESC/backdrop handling, animationsid="modal-title" (hardcoded expectation)const triggerRef = useRef()// After modal dismiss:setTimeout(() => triggerRef.current?.focus(), 100)
Preserve + Transition components (300ms default)For full-page/side-drawer modals, use modal-page.jsx with nesting support.
Use useAlerts() from app/src/ui/alerts.jsx:
const alerts = useAlerts()alerts.type3(t`Password updated successfully`, 'success')
Immutable Updates: Always spread entire objects in onChange handlers (never partial updates). Use Immer for complex nested updates.
app/src/app.css only for design system primitives (buttons, inputs, typography, etc.); never edit app/src/app.dist.css (generated)text-red-500cursor-not-allowed bg-light-gray-100--bg, --text-defaulttailwind.config.jstw-animate-cssrole="alert" and must be referenced via aria-describedbyaria-invalid="true" when errors existid="modal-title" for proper announcementUse Lingui macros (@lingui/react/macro):
import { Trans } from '@lingui/react/macro'import { useLingui } from '@lingui/react'// In componentconst { t, i18n } = useLingui()<Trans>Welcome message</Trans>const message = t`Dynamic message`// Date/number formattingi18n.date(new Date())i18n.number(1234.56)
Rules:
t() at module root - only inside components/functionsapp/src/locales/<locale>/messages.{po/js}pnpm extract to update .po catalogsbuild:i18n compiles catalogs (dev scripts watch and recompile)Expect union responses and check __typename:
const result = await updateUser({ variables: { id, input } })if (result.data.updateUser.__typename === 'InvalidFieldErrors') {// Error structure: { errors: [{ field, reason }] }const errorMap = mapValues(keyBy(result.data.updateUser.errors, 'field'),'reason')setErrors(errorMap)} else {// Success: result.data.updateUser is the User typehandleSuccess(result.data.updateUser)}
For new code: Use dedicated Context API or Apollo cache for sharing data between components.
Existing pattern (legacy from React Router 7 migration - avoid in new code):
// Some existing routes use useOutletContext - when reading existing code:const { initialUser, onChange } = useOutletContext()
When backend schema changes: node scripts/generate-graphql-schema (requires backend), restart editor/ESLint. ESLint uses schema to validate queries - regenerate if unknown field errors.
Testing standards and patterns are documented in .claude/rules/testing.md. This file is automatically loaded when working with test files (*.test.* or *.spec.*).
app/src/routes.jsx (React Router 7 createBrowserRouter)Navigate helper to preserve search paramsProtectedAppLayout for standard nav/header{path: '/users/:id',element: <Protected><AppLayout><UserPage /></AppLayout></Protected>,children: [{ path: 'edit', element: <UserEditModal /> }]}
app/src/pages* directory:pages-builder/ for configuration UIpages-runner/ for end-user runtimepages/ for system administrationpages-anonymous/ for public/unauthenticatedAppLayout if it needs standard nav/headerapp/src/routes.jsxProtected wrapper if authentication requirednode scripts/generate-graphql-schema (requires backend running)typePolicies only if custom merge/key needed (in app/src/gql/index.jsx)pnpm test:no-coverage and pnpm test:lintapp/src/app.css only for design system primitives (buttons, inputs, typography, etc.)app/src/app.dist.css (generated)pnpm build:css manually, orpnpm test:no-coverageapp/src/components/feature-flags.tsx?center-modal=feature-flagsapp/src/components/feature-flags.tsx (id, default, description)setFlag/clearFlag from @/components/feature-flags to toggle flag state.test/setup.js defaults if new flag should be on by defaultpnpm test:no-coverage and pnpm test:lintWhen removing permanently enabled feature flags:
Logic Analysis: For each conditional check, determine what the condition evaluates to when the flag is TRUE
if (flag) → condition is TRUE, keep the if-block contentif (!flag) → condition is FALSE, remove entire if-blockif (flag || other) → condition is FALSE, remove entire if-blockif (!flag && other) → condition is FALSE, remove entire if-blockif (flag && other) → simplifies to if (other)if (!flag || other) → simplifies to if (other)Variable Cleanup: After removing conditionals, look for variables that are no longer needed:
.filter(Boolean) when all elements are now unconditionalSpread Operator Arrays: When removing conditional spreads:
// Before:const arr = [A, ...(flag ? [B] : []), C].filter(Boolean)// After (flag=TRUE):const arr = [A, B, C] // Remove filter(Boolean) too
Self-Review: Apply the Self-Review Checklist to catch simplification opportunities
Wrap user-facing text in Lingui macros:
import { useLingui } from '@lingui/react'import { Trans } from '@lingui/react/macro';<Trans>New text</Trans>const { t } = useLingui()const msg = t`Dynamic message`
Use i18n.date(date) and i18n.number(num) for formatting
Run pnpm extract to refresh .po catalogs
Commit the .po file changes
Run pnpm build:i18n (or let dev watcher handle it)
Run pnpm test:no-coverage
app/src/__tests__@testing/*mountApp, renderPage, or renderGadgetConfig as appropriateMockley instance if default mocks insufficientawait loadLocale('en') before asserting translated textgetByRole)pnpm test:no-coverage for quick feedbackscripts/import-icons/ (never edit app/src/icons/index.tsx)node scripts/generate-graphql-schema (requires backend), restart editor afterscripts/generate-csp (when adding external domains)scripts/generate-browser-checkFull guide: app/src/formbot/gadgets/gadgets.mdx (requirements checklist, examples, three-repo structure)
Gadgets live in app/src/formbot/gadgets/. File structure:
app/src/formbot/gadgets/my-gadget/manifest.jsx # Ties everything togetheredit.jsx # Runtime edit (functional component + hooks)view.jsx # Display componentconfig.jsx # Configuration UIvalidation.jsx # validateShape (custom validator)utils.js # Business logic (formatting, parsing) - pure functionsicon.svg.jsx # SVG iconprogressive-disclosure.jsx # (optional) Conditional visibilityfilters.jsx # (optional) Document list filtering
Modern patterns (functional components, Tailwind, utils separation):
useState/useRef/useEffect, not class componentsutils.js (pure functions) - see Currency, PhoneNumber, Table gadgetsaria-labelledby, aria-describedby, aria-required, aria-invalidThird-party libraries: Only MIT/BSD/Apache 2.0 licenses - no GPL/AGPL/copyleft. Check package.json license field before adding dependencies.
Testing: Aim for 15-20+ tests (component, utils, edge cases, accessibility). Use renderGadgetConfig/renderGadgetEdit from @testing/setup.
Register in app/src/formbot/index.jsx:
import MyGadget from './gadgets/my-gadget/manifest'formbot.registerGadget('MyGadget', MyGadget)
Multi-repo: UI in builder-ui/app/src/formbot/gadgets, server validation in platform/lib/formbot/gadgets (Elixir), form processing in forms-api/server/lib/gadgets (Node.js)
Field reference detection: If your gadget/feature stores references to meta.createdBy or meta.submittedBy fields (including extended attributes), update app/src/pages-builder/form/extended-attribute-usage-utils.ts to detect those references. This prevents admins from removing fields that are in use. See existing patterns in that file.
Check DevTools Network → inspect __typename for union types. Verify query includes required fields. Regenerate schema if lint errors (node scripts/generate-graphql-schema). Check Apollo cache. Common: missing __typename checks, stale schema, cache merge issues (see type policies section).
Check user.ssoId: present = SSO-only, absent = password auth. Hide password fields when user.ssoId exists. Backend returns HTTP 400 if SSO user attempts password change.
id="modal-title" on heading or focus not returned to trigger (see Modal Usage)touched flags; error containers need role="alert"; reference via aria-describedby; set aria-invalid="true" on inputs with errorspnpm build:i18n, add await loadLocale('en') before assertionsapp/src/@testing/mocks/, use api.debug() to dump mock dataTZ=Etc/UTCpnpm test:no-coverage for targeted runs (full suite enforces thresholds)find* queries (async), follow Testing Library query prioritywaitFor or use find* queriesVerify PORT (default 52002), check backend running, try pnpm dev:docked, check port conflicts (lsof -i :52002), clear cache (rm -rf node_modules/.vite).
app.css not app.dist.css, run pnpm build:csspnpm extract → pnpm build:i18npnpm build (CSS → i18n → JS)pnpm test:lintpnpm test:prettierpnpm format (applies Prettier)pnpm test:licensemasterpnpm run to see all available scriptsEvery PR must have a "Change type" label:
change_type/application - Use for most PRs (code changes, features, bug fixes, tests)change_type/infrastructure - Use only for configuration file changes (CI/CD, build config, etc.)Add labels when creating PRs:
gh pr create --title "Title" --body "Description" --label "change_type/application"
Always run: pnpm test:no-coverage, pnpm test:lint, pnpm test:prettier
Check ESLint on changed files as you work: After modifying or creating files, run pnpm eslint "path/to/changed/file.jsx" before moving on.
Task-specific requirements covered in Task Recipes sections above (GraphQL schema regen, i18n extract, CSS build, etc.)
Don't: edit generated files, call t() at module root, create unnecessary files, add unrequested features, partially update state, test implementation details, assume methods exist
Last Updated: 2025-11-28 Maintained By: AI Agents (update as project evolves)