# Simulator Smart Forms

> 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".

- Skill: `corezoid/simulator-smart-forms` (Agent Skill)
- Install (CLI): `npx skillmds@latest add corezoid/simulator-smart-forms`
- Raw SKILL.md: https://api.skillmd.com/api/skills/corezoid/simulator-smart-forms/raw
- Safety review: PASS (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: corezoid (https://skillmd.com/u/corezoid)
- Updated: 2026-08-19
- Page: https://skillmd.com/skills/corezoid/simulator-smart-forms

---


# 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
    → 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 }

4.  Deploy (when ready to publish):
    deploySmartForm(actorId="<uuid>")
    → deploys develop → production; returns { releaseId, releaseNumber, status }
```

---

## 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"
            }
          ]
        }
      ]
    }
  ]
}
```

### 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`** — only `header` and `content` (plus `modalHeader` for modals).
> Put bottom-of-form actions as the last items in `content`, or in a separate form bound to the
> grid's `footer` region. (The grid's `components.footer` above 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): `row` renders as a CSS **table** so its items
> **do not wrap** (use `display:inline-block` items directly in `content` for a wrapping grid);
> `styleClass` on a `row` is dropped from the DOM (style leaf items, not the `row`); a `radio`/`select`
> option dot is a JSS-hashed `<i class="i-icon-…">` (not the `<input>`) — restylable via
> `[class*="icon"]`/`[class*="content"]` (match by substring; the `i-` prefix isn't stable across builds) but that couples to renderer internals, so `button`s are
> the version-proof alternative for card pickers; and a field with `visibility:"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

| `class` | Notes |
|---|---|
| `table` | `head: [{id,title}]`, `body: [{<id>: value}]`; `type`: `default` `radio` `check`; `submitOnChange`, `submitOnScroll` |
| `tab` | `options: [{id,title}]`; `value` = selected id; `submitOnChange` |
| `stepper` | `options: [{id,title}]`; `value` = step; `extra.direction` |
| `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]`. |

### 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

1. **Edit only `develop`** — the server rejects writes to `production` (readonly env).
2. **Run `pullSmartForm` before editing** — establishes `.manifest.json` needed by `pushSmartForm`.
3. **Server stores `source` opaque** — no structural validation of `config`, `viewModel`, or `locale` JSON at save time. Validate page JSON against the CDU protocol before pushing.
4. **Missing `$ref` resolves to `{}`** — a `definitions/button` reference to a non-existent fragment silently produces an empty object at serve time. Always verify definition names.
5. **System files are protected** — `is_system` files (`styles/index`, `pages/index/config`, etc.) cannot be renamed or deleted. Edit their content freely; rename/delete will fail.
6. **CSS is Less** — `styles/index` is compiled with Less at serve time, wrapped in `.cdu-page {}`. Use Less syntax; `@import "pages/<id>/style"` imports page-level stylesheets.
7. **Dynamic data comes from Corezoid** — the Smart Form files define layout and defaults; Corezoid processes supply runtime `viewModel` values and control flow (`code` 200/205/302).
8. **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 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` / `appSettings` on `createReaction` / `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` (the `appId` + `extra.linkedActorId` + `[application=…]` chip)
> and `docs/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 |

