Always use the project's crafted components first. Each project has its own crafted components defined in Project/Sources/WebForms/crafted_components.json. Before generating component JSON from scratch, read this file from the current project. If the developer has defined reusable crafted components there, use them in your page designs. Only generate raw component structures when no matching crafted component exists in that project.
Use existing pages as reference. If the project contains .WebForm files in Project/Sources/WebForms/, read them to understand the project's conventions — component structure, styling patterns, naming, event wiring, and datasource usage. Match the existing style and patterns when generating or modifying pages. Only generate from scratch (using the schemas in Schemas/) when no existing pages are available.
Keep i18n in sync. When using i18n translations in WebForms, always update Project/Sources/Shared/i18n.json with any new translation keys. Ensure all text content is translated for every supported language. In the WebForm JSON, i18n involves three parts:
a) props.doc — use "type": "i18n" inline spans:
{ "type": "i18n", "children": [{ "text": "Login", "bold": true, ... }], "__i18n": "connect", "id": 1773401140052, "__baseValue": "Connexion SSO" }
The __i18n value is the key in i18n.json, __baseValue is the base text, and children.text is the default-language display text.
b) custom.__t — translation metadata:
__t.doc contains metadata with { children: [null, { children: [{ text: { key: "<i18n_key>", default: "<default_text>" } }] }] }, plus per-language empty doc placeholders (e.g. "en": { "doc": [] }, "fr": { "doc": [] }).__t.text is { "key": "<i18n_key>", "default": "<default_text>" }.c) custom["i18n:<lang>"] — per-language translated content:
"i18n:en": { "doc": [{ "type": "paragraph", "children": [{ "text": "SSO Login", "bold": true }] }] }"i18n:en": { "text": "Connect with SSO" }You must include an i18n:<lang> entry for every supported language in the project.
Use WebForm Loaders inside Tabs. When using the Tabs component, do not put all tab content directly in the same page. Instead, place a WebForm Loader (page loader) inside each tab panel and load the tab's content from a separate .WebForm page. This keeps the studio responsive and improves runtime performance by loading each tab's data and elements on demand. If a datasource (Qodly source) needs to be accessed across multiple tabs, declare it as a shared datasource so all tab pages can reference it.
For detailed Qodly documentation (components, events, datasources, styling, roles, permissions, deployment, etc.), read the qodly-docs skill (sibling folder qodly-docs/ in the same repo, or ~/.cursor/skills/qodly-docs/ when installed). Key sections:
4DQodlyPro/pageLoaders/components/ — individual component docs (DataTable, Tabs, Button, Text, SelectBox, etc.)4DQodlyPro/pageLoaders/events/ — event management and binding actions4DQodlyPro/pageLoaders/qodlySources.md — Qodly datasources4DQodlyPro/pageLoaders/styling.md — CSS and styling4DQodlyPro/pageLoaders/craftedComponents.md — crafted components4DQodlyPro/localization.md — i18n / localization4DQodlyPro/roles/ — roles, privileges, and permissionsIntegrations/customComponent/ — custom component developmentQodly pages are web forms built in Qodly Studio that run on top of 4D Server. They use:
WebForm (Page)
├── Data Sources (bindings to backend data)
├── Components (UI elements from craft library)
│ ├── Properties (configuration)
│ ├── Data bindings (linked to data sources)
│ └── Events (onClick, onChange, onLoad, etc.)
└── Functions (exposed 4D class functions called by events)
Data sources connect UI components to backend data. Types:
| Type | Description | Example |
|---|---|---|
| Entity | Single record | ds.Employee.get(42) |
| Entity Selection | List of records | ds.Employee.all() |
| Scalar | Simple value (text, number, bool) | A search term, a counter |
| Object | Structured data | Configuration, form state |
| Collection | List of values | Dropdown options |
Data sources are declared on the page and bound to components:
dataSource, an Input's value)Use the onLoad event of the page to call an exposed function that populates data sources:
// EmployeePage.4dm (or a DataClass function)
exposed Function loadPageData() -> $result : Object
$result := New object()
$result.employees := ds.Employee.query("status = :1"; "active")\
.orderBy("lastName asc")\
.toCollection("firstName, lastName, department.name, salary")
$result.departments := ds.Department.all().toCollection("name, id")
Components emit events that call server-side functions:
| Event | Triggers When |
|---|---|
onLoad | Page loads |
onClick | User clicks component |
onChange | Value changes (inputs, selects) |
onSubmit | Form submission |
onSelect | Row/item selected |
onSort | Column sort requested |
onHeaderClick | Table header clicked |
exposed function on a 4D classComponent: Button with onClick event
Calls: ds.Employee.search($criteria)
// Employee.4dm (DataClass)
exposed Function search($criteria : Object) -> $result : cs.EmployeeSelection
var $query : Text := ""
var $params : Object := New object()
If ($criteria.name # Null) && ($criteria.name # "")
$query := "firstName = :name OR lastName = :name"
$params.name := $criteria.name + "@"
End if
If ($query = "")
$result := This.all()
Else
$result := This.query($query; $params)
End if
Two panels: a list (DataTable/Matrix) on the left, detail form on the right.
employeeList (entity selection), selectedEmployee (entity)employeeListselectedEmployee to the clicked row's entityselectedEmployee.firstName, etc.selectedEmployee.save()searchTerm (scalar text), results (entity selection)searchTermonClick calls exposed function with searchTerm valueresults data sourcecurrentEmployee (entity)ds.Employee.new() to create empty entitycurrentEmployee.save()currentEmployee.drop()currentEmployee.reload()All functions called from Qodly pages must be exposed:
// DataClass level
exposed Function myFunction($param : Text) -> $result : Object
// Entity level
exposed Function myEntityFunction() -> $result : Object
// Singleton level (for non-data operations)
exposed Function myUtility($input : Object) -> $result : Object
Qodly uses a class-based CSS system:
Craft components are the building blocks for Qodly pages. See craft-components.md for the component catalog with properties, events, and usage patterns.
The backend for Qodly pages runs on 4D. Consult docs at .cursor/skills/4d-docs/ for:
.cursor/skills/4d-docs/REST/ — how exposed functions are called via REST, filtering, sorting, entity sets.cursor/skills/4d-docs/ORDA/ — data model classes, privileges, entity selections (powers data sources).cursor/skills/4d-docs/API/ — DataClassClass, EntityClass, EntitySelectionClass (for exposed functions).cursor/skills/4d-docs/WebServer/ — server configuration underlying Qodly.cursor/skills/4d-docs/Users/ — user management and access controlRead the relevant files when designing Qodly pages to ensure data bindings and exposed functions follow the correct API.