Skill v1.0.1
currentAutomated scan100/100+3 new
version: "1.0.1" name: simulator-smart-forms description: > Simulator.Company Smart Form (CDU / Script / Application) specialist. Use when the user wants to create, edit, inspect, or deploy a Smart Form; work with its pages, locale, viewModel, styles, definitions, or widgets; understand the CDU page protocol; pull or push form files; manage releases; or ask questions about the app_content / applications / releases / file_history APIs. Activate on: "smart form", "CDU", "script application", "pullSmartForm", "pushSmartForm", "page config", "viewModel", "locale", "deploy smart form", "edit page layout", "add component to CDU", "rollback release".
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" formactors are instances of it carries pages/styles/i18n as versioned filesaccount 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 timeproduction/.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 dir2. 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
{"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"}]}]}]}
Grid
{"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
{"id": "info","title": "Details","styleClass": "card","visibility": "visible", // "visible" | "disabled" | "hidden""sections": [ /* Section[] */ ]}
Section
{"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 thegrid'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 indocs/user-flows/cdu-page-protocol.md§12):rowrenders as a CSS table so its itemsdo 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 arethe version-proof alternative for card pickers; and a field withvisibility:"visible"but hidden viaCSS 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`, never `id`, is the key in an option or column list —table.head[],tab.options[],stepper.options[]. Cells of a table row live inbody[].options[], not asdirect 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.
// 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-itemvisibility(inheader,modalHeader, andcontent) may bea pure view-model placeholder such as"{{graphPreviewVisibility}}"; the server resolves ittovisible|disabled|hiddenbefore the page reaches the client. Malformed or embedded forms("state-{{x}}","{{ x }}", a bare enum typo) are rejected at push, andfooteris notserver-rendered so a placeholder there is rejected too. This is initial/server-render binding, nota client-side expression: to reveal one field after another changes, usesubmitOnChangeplus a200changesresponse.⚠️ Alabelvalue may never be the empty string (nor animagean empty src) — the rendererrejects 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
{"hello": { "en": "Hello", "uk": "Привіт" },"submit": { "en": "Submit", "uk": "Надіслати" }}
viewModel file format
{"defaultName": "Anonymous","maxItems": 10}
definitions fragment format
{"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:
[{"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", // optionalsharedWith="userList", // optional, default userListapiLogin="...", // optional — set Corezoid binding later if omittedapiSecret="...",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 versiongetFileVersion(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 envlistTrash(actorId="<uuid>") // develop (default)listTrash(actorId="<uuid>", env="production")→ list of { objectId, title, objType, deletedAt, … }// Restore a deleted objectrestoreFromTrash(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 to
production(readonly env). - Run `pullSmartForm` before editing — establishes
.manifest.jsonneeded bypushSmartForm. - Server stores `source` opaque — no structural validation of
config,viewModel, orlocaleJSON at save time. Validate page JSON against the CDU protocol before pushing. - Missing `$ref` resolves to `{}` — a
definitions/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 productiondeploySmartForm(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 on
createActor/
updateActor; the actor's card then renders/runs that smart form. `appSettings` tunes 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 seesimulator-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 |