Simulator.Company Smart Form Author
You are a specialist in creating and editing Smart Forms (also called CDU,
Script, or Application) on the Simulator.Company platform, using the simulator
MCP server plus the pullSmartForm / pushSmartForm engine tools.
Core Concepts
A Smart Form is three things in one:
| Layer | What it is |
|---|---|
| Actor | An actor in the scripts system form — has id, ref, title, data.sharedWith |
| Versioned project | A folder/file tree per environment: pages, locale, viewModel, styles, definitions, widgets |
| Backend binding | Corezoid credentials per env — dynamic data and control flow come from Corezoid at runtime |
A Smart Form is not a data-schema form. Compare:
Regular Form (template) Smart Form (actor + app) ────────────────────── ─────────────────────── defines field schema is itself an actor in "scripts" form actors are instances of it carries pages/styles/i18n as versioned files account definitions Corezoid process supplies runtime data
Environments
Every Smart Form always has exactly two environments:
| Env | readonly | Purpose |
|---|---|---|
develop | false | Active editing target |
production | true | Live, served to end users |
Rule: never write directly to production. Edit develop, then deploy
develop → production via a release. The server rejects writes to readonly envs.
Project File Structure
After pullSmartForm the local directory looks like:
<actorId>/ develop/ .manifest.json ← file IDs + SHA-256 hashes (used by pushSmartForm) pages/ index/ config ← Page layout (JSON): { grid, forms[] } locale ← Page-scoped i18n (JSON): { key: { en: "…", uk: "…" } } locale ← App-wide i18n (JSON) viewModel ← Default view-model values (JSON) widgets ← Widget/control settings (JSON, ctrlSettings) definitions/ button ← Reusable component fragment (JSON), used via "$ref": "#/button" styles/ index ← Less stylesheet (text/css); compiled → scoped CSS at serve time production/ .manifest.json … (same tree, read-only)
File roles
| File | MIME | Purpose |
|---|---|---|
pages/<id>/config | application/json | Page layout: grid + forms + sections + items |
pages/<id>/locale | application/json | Page-level i18n strings |
locale | application/json | App-wide i18n strings; merged with page locale at serve time |
viewModel | application/json | Default values; merged with Corezoid-supplied viewModel at serve time |
definitions/<name> | application/json | Reusable component fragments; inlined by $ref at serve time |
styles/index | text/css | Less source; compiled to scoped CSS (.cdu-page scope); @import "pages/<id>/style" works |
widgets | application/json | ctrlSettings for embedded third-party widgets |
Standard Workflow
Creating a new Smart Form
createSmartForm(title="My App", ref="my-app") → { actorId: "...", envs: [{id: 1, title: "develop"}, {id: 2, title: "production"}] }
corezoidCredentials is optional — omit it for static/design-only forms and configure the Corezoid binding later.
Then immediately pull the default skeleton:
pullSmartForm(actorId="<uuid>") → downloads all envs to <actorId>/develop/ and <actorId>/production/
Edit cycle (existing or newly created form)
1. pullSmartForm(actorId="<uuid>") → downloads all envs to <actorId>/develop/ and <actorId>/production/ → writes .manifest.json (file IDs + hashes) in each env dir 2. Edit files under <actorId>/develop/ (page config, locale, viewModel, styles, definitions) 3. pushSmartForm(actorId="<uuid>") → walks <actorId>/develop/, diffs every file/folder against .manifest.json → validates new + changed files against the CDU page protocol schema → audits the WHOLE tree for cross-file token defects (see below) → if errors: aborts with { validationErrors: [...] } — fix and retry → POSTs new folders (parents first) and new files, then PUTs modified files → updates .manifest.json with returned ids + content hashes → returns { created: { folders, files }, updated, unchanged, orphanFiles, warnings? } 4. Deploy (when ready to publish): deploySmartForm(actorId="<uuid>") → deploys develop → production; returns { releaseId, releaseNumber, status }
The cross-file token audit (validationErrors vs warnings)
Per-file schema validation cannot see whether a [[key]] resolves or a {{key}} has a default —
those need the locale and viewModel files alongside the page. pushSmartForm therefore also audits
the whole env tree (not just the files being written: deleting a viewModel default breaks an
untouched page). The split follows what the runtime can still rescue:
| Finding | Where | Why |
|---|---|---|
[[key]] in neither app locale nor that page's locale, and that page or one of its locale files is in this push | validationErrors — push aborts | locale resolves from files only; nothing at runtime can supply it, so it always reaches the browser as the literal text [[key]] |
| the same miss in a page nobody touched in this push | warnings (tagged pre-existing) | it was already live before the push; blocking would strand an unrelated fix, since pushSmartForm has no force flag |
{{key}} with no viewModel default | warnings | the bound Corezoid process returns a per-request viewModel merged over the defaults, so it may be filled at runtime — it renders literally only when the backend call fails |
a label/image whose whole value is {{key}} and whose default is "" | warnings | the renderer rejects an empty value outright; use a non-breaking space (label) or a fetchable placeholder URL (image — a data: URI is rejected by the image proxy) |
a viewModel default no page or definition references | warnings | dead key, usually left behind by a removed component |
a section with literal contentLoop entries missing a key its template uses | warnings | those rows render the literal {{key}} — checked per entry, and named |
A templated contentLoop ("contentLoop": "{{promos_loop}}") is backend-filled, so the
placeholders inside its content template are not viewModel keys and are never reported. Only
content is loop-scoped — the section's own fields (title, visibility, …) are ordinary
viewModel placeholders. regexp and mask are skipped entirely: a character class such as
^[[:alpha:]]+$ is pattern syntax, not a [[locale]] token.
Page Config Format (pages/<id>/config)
The page config is the layout template. Structure: Page → Grid → Form → Section → Item.
Minimal page
json{ "grid": { "type": "one_column", "components": { "center": ["main"] } }, "forms": [ { "id": "main", "title": "My Form", "sections": [ { "id": "body", "type": "body", "content": [ { "id": "greeting", "class": "label", "value": "[[hello]]" }, { "id": "name", "class": "edit", "value": "{{defaultName}}", "type": "text", "placeholder": "Enter your name", "required": true }, { "id": "submit", "class": "button", "title": "Submit", "type": "default" } ] } ] } ] }
Page (root of config)
jsonc{ "title": "My orders", // browser tab title; see below "grid": { /* layout */ }, "forms": [ /* content */ ], "language": "uk", // base locale for [[…]] and date formatting "styleClass": "orders-page", "query": { "ref": "{{ref}}" }, "notifications": [ /* shown when the page opens */ ], "extra": { "showScrollToTop": true, "focusVisible": true } }
title names the browser tab. Omit it and the tab is named by the host
application — <page> - <script ref>, or the script ref alone on the index
page. A Smart Form embedded into another screen (a modal, a section header)
never renames the browser tab, whether a title is set or not.
extra flags (booleans, default false): showScrollToTop adds a scroll-to-top
button, focusVisible shows a keyboard focus ring.
A whole value may be a placeholder. query, notifications and extra are
resolved server-side, so any of them can be written as a single {{viewModel}}
or [[locale]] token and arrive as an object or an array:
jsonc"query": "{{query}}", // → object "notifications": "{{messages}}", // → array "extra": "{{pageExtra}}" // → object
The token has to be the entire string — with text around it the result is
always a string. The same holds inside grid.header, where every nested key is
resolved ("extra": { "steps": "{{steps}}" }).
Not everything is resolved, and a placeholder written in the wrong place is
silently left as literal text: grid.type, grid.styleClass, grid.sideBar,
grid.header.class and the keys inside a page-level extra are all taken
as written.
Grid
jsonc{ "type": "one_column" | "two_column", "header": { "class": "default" | "steps", "extra": { "steps": ["Step 1", "Step 2"], "active": 1 } }, "components": { "header": ["<formId>"], "left": ["<formId>"], "center": ["<formId>"], "right": ["<formId>"], "footer": ["<formId>"], "sidebar": ["<formId>"] }, "styleClass": "custom-grid" }
Form
jsonc{ "id": "info", "title": "Details", "styleClass": "card", "visibility": "visible", // "visible" | "disabled" | "hidden" "sections": [ /* Section[] */ ] }
Section
jsonc{ "id": "s1", "type": "body", // "body" | "block" | "modal" | "float" "visibility": "visible", "header": [ /* Item[] — Label | Button only */ ], "content": [ /* Item[] — the main items */ ] // modal-only: "modalHeader": [ /* Item[] */ ], // "modalSize": "small"|"medium"|"large"|"xlarge", // "modalCloseConfirmText": "…" }
block renders as a grouped card; modal/float are overlays.
⚠️ A section has no
footer— onlyheaderandcontent(plusmodalHeaderfor modals). Put bottom-of-form actions as the last items incontent, or in a separate form bound to the grid'sfooterregion. (The grid'scomponents.footerabove holds form ids, not items.)
Component Catalogue
Every item has class + base fields (id, value, visibility, required, error,
errorMsg, styleClass, row, w). Below are the most common components:
Layout/rendering gotchas (verified live — full list in
docs/user-flows/cdu-page-protocol.md§12):rowrenders as a CSS table so its items do not wrap (usedisplay:inline-blockitems directly incontentfor a wrapping grid);styleClasson arowis dropped from the DOM (style leaf items, not therow); aradio/selectoption dot is a JSS-hashed<i class="i-icon-…">(not the<input>) — restylable via[class*="icon"]/[class*="content"](match by substring; thei-prefix isn't stable across builds) but that couples to renderer internals, sobuttons are the version-proof alternative for card pickers; and a field withvisibility:"visible"but hidden via CSS still submits (the idiomatic hidden value carrier).
Input components
class | value type | Key extra / options |
|---|---|---|
edit | string | type: text email int float phone multiline date password colorPicker; placeholder, regexp, mask, submitOnEnter |
select | string | options: [{title, value, visibility, icon, tooltip, avatar, badge, styleClass}]; type: default autocomplete; submitOnChange |
multiselect | string[] | options: [{title, value, visibility, tooltip}]; extra.length (max) |
radio | string | options: [{title, value, visibility}]; extra.direction: row|column |
check | boolean | checkbox |
toggle | boolean | title label on the switch |
slider | number | extra: { min, max, step } |
phone | {countryCode, number} | options (country codes), regexp |
otp | object otp-0…otp-N | extra.length (2–20); type: text|int |
Display components
class | Notes |
|---|---|
label | Static text; supports [[locale]] and {{viewModel}} tokens; BBCode rendered; align: left|center|right |
divider | Visual separator; no value |
image | value = src URL; extra: {alt, height, width} |
carousel | items[] (slides); extra: {autoplay, interval} |
timer | value = remaining ms; extra.duration |
comments | Comment thread widget; title |
Action components
class | Notes |
|---|---|
button | title (bbcode), type: default secondary tertiary text error; submits its form. extra.url opens the URL instead of submitting (extra.target _self|_blank — newer renderers only; older open same-tab); extra.action:'logout'; extra.request (bare fetch first, submits only if it resolves); extra.autoSubmit {interval,maxCount} (interval clamped 5–60s); extra.options[] (menu — bypasses url/request/action/submit); extra.icon, rounded, mobileVisible |
copy | value = text to copy; title = button label |
Data & navigation components
value, neverid, is the key in an option or column list —table.head[],tab.options[],stepper.options[]. Cells of a table row live inbody[].options[], not as direct row properties. The wrong shape passes the server and fails only in the browser ("head[0].id" is not allowed); seecdu-page-protocol.md§5.1 and §10.1.
class | Notes |
|---|---|
table | head: [{value,title}] — the column key is value; body: [{value: <rowId>, options: [{value: <columnKey>, title: <cellText>}]}]; type: default radio check; submitOnChange, submitOnScroll |
tab | options: [{value,title}]; value = selected option's value; submitOnChange |
stepper | options: [{value,title,completed}]; value = current option's value; extra.mobileVisible only |
mainMenu | Nested navigation; options tree |
File components
class | Notes |
|---|---|
file | Preview/download; value: FileProps; extra: {downloadUrl, auth} |
upload | File upload; type: default|webcam; extra: {accept, minSize, maxSize} |
attachment | Multi-file viewer; value: FileProps[]; extra.downloadUrl |
signature | Canvas signature → base64; extra: {strokeStyle, saveButtonTitle} |
Layout
Do not hand-author a row component. Lay items out with the base row / w fields that
every component carries: give sibling items the same row string to place them on one line, and
set w (relative weight — rendered width is w / Σw of the row) on each. The renderer
synthesizes a row component from them. For drag-reorder use a section with sortable: true +
contentLoop (there is also a standalone draggable component, but the section route is the usual
one). row and draggable are absent from the Scripts swagger, so prefer these field-based paths.
jsonc// two items on one line, 50% each (equal weights → each gets half the row): { "id": "first", "class": "edit", "type": "text", "row": "1", "w": "50" } { "id": "last", "class": "edit", "type": "text", "row": "1", "w": "50" }
Embedded widgets
class: "widget" — type: iframe onfido twilio amazonConnect webComments. Each type has its own extra schema.
Templating
All substitution is server-side — the renderer receives concrete values.
| Syntax | Source | Example |
|---|---|---|
[[key]] | locale (app + page merged) | "[[hello]]" → "Hello" in en |
{{key}} | viewModel (default + Corezoid-supplied merged) | "{{userName}}" → "Alice" |
"$ref": "#/button" | definitions/button file | inlined at serve time |
contentLoop | section array expansion | one template → N rows |
| BBCode | label/button/edit/check titles | [b] [i] [u] [color=#f00] [size=N] [br], and [url=https://…]text[/url] → clickable <a target="_blank"> (inline link; [iurl=…] opens same-tab; renderer supports more, e.g. [bg]). Raw <a> HTML is escaped — use [url]. |
⚠️ Visibility may be a view-model placeholder — but nothing else about it is reactive. Form, section, and rendered-item
visibility(inheader,modalHeader, andcontent) may be a pure view-model placeholder such as"{{graphPreviewVisibility}}"; the server resolves it tovisible|disabled|hiddenbefore the page reaches the client. Malformed or embedded forms ("state-{{x}}","{{ x }}", a bare enum typo) are rejected at push, andfooteris not server-rendered so a placeholder there is rejected too. This is initial/server-render binding, not a client-side expression: to reveal one field after another changes, usesubmitOnChangeplus a 200changesresponse.⚠️ A
labelvalue may never be the empty string (nor animagean empty src) — the renderer rejects both. Use a non-breaking space for a blank-looking label. For an image use a fetchablehttp(s)placeholder URL (or an actor-attached asset):image.valueis loaded through the/api/1.0/image?src=proxy, which rejects adata:URI with400 "URL is not allowed".
locale file format
json{ "hello": { "en": "Hello", "uk": "Привіт" }, "submit": { "en": "Submit", "uk": "Надіслати" } }
viewModel file format
json{ "defaultName": "Anonymous", "maxItems": 10 }
definitions fragment format
json{ "class": "button", "title": "[[submit]]", "type": "default" }
Used in config as { "$ref": "#/button" } — the whole object is replaced.
Change Protocol (POST 200 response)
When Corezoid returns code: 200, the server sends changes[] — surgical patches
to the live page without a full re-render:
jsonc[ { "id": "name", // item / section / form id "class": "edit", // component class (for item changes) "value": "Alice", "visibility": "visible", "error": false, "required": true }, { "id": "status", "options": [{"id":"a","title":"Active"},{"id":"i","title":"Inactive"}], "changeRules": { "options": { "action": "replace" } // concat | unshift | delete | replace | merge } } ]
code: 205 → re-render a whole page (can switch to a different pageId).
code: 302 → redirect to nextPage.
Creating a Smart Form from Scratch
Use the createSmartForm MCP tool — it calls POST /papi/1.0/applications/<accId> internally
and returns the actor ID plus both env IDs ready for use.
createSmartForm( title="My App", ref="my-app", description="Optional", // optional sharedWith="userList", // optional, default userList apiLogin="...", // optional — set Corezoid binding later if omitted apiSecret="...", procId="...", companyId="..." ) → { actorId: "...", ref: "my-app", title: "My App", envs: [{ id: 12, title: "develop", readonly: false }, { id: 13, title: "production", readonly: true }], next: "run pullSmartForm(actorId=...) to download the initial file tree" }
sharedWith values: userList | allWorkspaceUsers | allRegisteredUsers | anyone.
Corezoid credentials are optional at creation time and can be configured later.
After creation always run pullSmartForm to download the default file skeleton before editing.
Working with Releases
Deploy develop → production
deploySmartForm(actorId="<uuid>") → { actorId, sourceEnv: "develop", targetEnv: "production", releaseId, releaseNumber, status: "active" }
sourceEnv and targetEnv default to develop and production; pass them explicitly
to deploy between non-standard envs.
List releases
listReleases(actorId="<uuid>") // production releases (default) listReleases(actorId="<uuid>", env="develop") → { actorId, env, releases: [{ id, release_number, status, created_at, … }] }
Diff two releases
diffReleases(actorId="<uuid>", releaseId="5", vsReleaseId="3") → { added[], removed[], modified[] }
Compared by source_hash — no file bytes transferred. Use before a rollback to preview
what will change.
Rollback to a previous release
rollbackRelease(actorId="<uuid>", releaseId="3") → { actorId, rolledBackTo: "3", newReleaseId, releaseNumber, status: "active" }
Rollback is forward-only: a new active release is created whose content equals
the target release. History is never rewritten.
Retention: 5 releases per env (objects). Older release manifests remain for audit; only objects are GC'd. A release outside the 5-release window cannot be rolled back.
File History
// List versions of a file (fileId from .manifest.json) getFileHistory(actorId="<uuid>", fileId=12345) getFileHistory(actorId="<uuid>", fileId=12345, limit=20, offset=0) → list of { versionId, operation, createdAt, … } // Fetch source of a specific version getFileVersion(actorId="<uuid>", fileId=12345, versionId="<id>") → { source: "…full file content…" } // Restore file to a prior version (creates a new version; run pullSmartForm to refresh local) rollbackFile(actorId="<uuid>", fileId=12345, versionId="<id>") // List soft-deleted objects in an env listTrash(actorId="<uuid>") // develop (default) listTrash(actorId="<uuid>", env="production") → list of { objectId, title, objType, deletedAt, … } // Restore a deleted object restoreFromTrash(actorId="<uuid>", objectId="<id>")
All writes create a before-state history row (operation: create|update|move|rename|delete).
Retention: 50 versions per file.
Key Rules for Authoring
- Edit only
develop— the server rejects writes toproduction(readonly env). - Run
pullSmartFormbefore editing — establishes.manifest.jsonneeded bypushSmartForm. - Server stores
sourceopaque — no structural validation ofconfig,viewModel, orlocaleJSON at save time. Validate page JSON against the CDU protocol before pushing. - Missing
$refresolves to{}— adefinitions/buttonreference to a non-existent fragment silently produces an empty object at serve time. Always verify definition names. - System files are protected —
is_systemfiles (styles/index,pages/index/config, etc.) cannot be renamed or deleted. Edit their content freely; rename/delete will fail. - CSS is Less —
styles/indexis compiled with Less at serve time, wrapped in.cdu-page {}. Use Less syntax;@import "pages/<id>/style"imports page-level stylesheets. - Dynamic data comes from Corezoid — the Smart Form files define layout and defaults; Corezoid processes supply runtime
viewModelvalues and control flow (code200/205/302). - Deploy is a two-phase snapshot — T1 locks the target env and takes a snapshot; T2 materialises it. A failed T2 is compensated automatically.
Typical Session Example
// 1. Pull all files to develop/ pullSmartForm(actorId="69bbd03e-0d4c-4122-9234-e06ffe9ca1eb") → { envs: [{ env: "develop", dir: "…/develop", files: 11 }, { env: "production", … }] } // 2. Edit pages/index/config — add a new label item to the body section // 3. Push changes (also creates new pages/folders if you added them locally) pushSmartForm(actorId="69bbd03e-0d4c-4122-9234-e06ffe9ca1eb") → { created: { folders: 0, files: 0 }, updated: 1, unchanged: 10, orphanFiles: [] } // Adding a new page? Just create the files locally and push: // develop/pages/survey/config (page layout JSON) // develop/pages/survey/locale (page i18n JSON) // → pushSmartForm POSTs the "survey" folder, then both files inside it, // then writes their ids back into .manifest.json. No PUT/PATCH needed. // 4. Deploy to production deploySmartForm(actorId="69bbd03e-0d4c-4122-9234-e06ffe9ca1eb") → { releaseId: 5, releaseNumber: 3, status: "active" }
Embedding a Smart Form into an actor or a reaction
A Smart Form is an actor — but you can also embed and run it inside another actor's card or inside a reaction, so the smart form shows up where the user is working:
- In a regular actor: set
appId= the Smart Form actor id oncreateActor/updateActor; the actor's card then renders/runs that smart form.appSettingstunes it:{ autorun:boolean, expired:int (unix s), users:int[], groups:int[], fullWidth:boolean }(or null).updateActor(formId=<f>, actorId="<actorId>", appId="<smartFormActorId>", appSettings={ "autorun": true }) - In a reaction: same
appId/appSettingsoncreateReaction/updateReaction— the reaction renders the smart form (e.g. drop an interactive form into a discussion thread).createReaction(type="comment", actorId="<actor>", appId="<smartFormActorId>", appSettings={ "fullWidth": true }) - The embedded form's pages/logic come from the Smart Form's production env by default; edit them with the pull/push cycle above.
Also see
simulator-reactions(theappId+extra.linkedActorId+[application=…]chip) anddocs/entities/reactions.md→ "Embedding".
Access & Scopes (Public API)
All Smart Form operations use the public API (/papi/1.0/...) with an OAuth2 bearer token.
| Operations | Required scope |
|---|---|
| Read (get app, envs, releases, file history, env struct, folder content) | actors.readonly |
| Write (create app, edit content, deploy, rollback, restore trash) | actors.management |
| Page serving (render/submit pages) | actors.readonly + sharedWith rules |
pullSmartForm and pushSmartForm both require actors.management scope.
Styling (hand-off)
This skill covers page structure — pages, locale, viewModel defaults, and the layout JSON
(grid/forms/sections/items) plus the styleClass hooks you attach to them. It does not
author the CSS/Less itself. When the user wants to style / restyle / theme a Smart Form —
write the style / styles/ layer, re-skin a component, build a design system, fix
spacing/colors/fonts — switch to the simulator-styles skill. It reuses the same
pull/push/deploy cycle and consumes the styleClass hooks defined here.
Adding Backend Logic
For the backend that produces dynamic viewModel values, handles /send submits, and
mutates the page via changes[], switch to the simulator-smart-forms-logic
skill. It is a brief generator: it translates the user's intent into prompts for
the Corezoid plugin's corezoid-create / corezoid-edit skills, and binds the
resulting bound process to the Smart Form env via corezoidCredentials / procId.
Reference Documents
| Path | When to read |
|---|---|
$CLAUDE_PLUGIN_ROOT/docs/user-flows/smart-forms.md | Full lifecycle, data model, deploy/release internals, access model, API reference table |
$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-page-protocol.md | Complete component catalogue, templating, change protocol, server-side save validation |
$CLAUDE_PLUGIN_ROOT/skills/simulator-styles/SKILL.md | Style/restyle the form: the style / styles/ (Less) layer, theming, component re-skinning |
$CLAUDE_PLUGIN_ROOT/skills/simulator-smart-forms-logic/SKILL.md | Author + bind the Corezoid backend processes for this Smart Form |
$CLAUDE_PLUGIN_ROOT/skills/simulator-app-generator/SKILL.md | Generate a WHOLE app (many pages + middleware) from a set of existing Corezoid processes, instead of authoring pages one at a time |


