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", ...
Installs into .claude/skills of the current project.
Are you the author of Simulator Smart Forms?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/corezoid-simulator-smart-forms)
---
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" 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`** — 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
> **`value`, never `id`, is the key in an option or column list** — `table.head[]`,
> `tab.options[]`, `stepper.options[]`. Cells of a table row live in `body[].options[]`, not as
> direct row properties. The wrong shape passes the server and fails only in the browser
> (`"head[0].id" is not allowed`); see `cdu-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` (in `header`, `modalHeader`, and `content`) may be
> a **pure** view-model placeholder such as `"{{graphPreviewVisibility}}"`; the server resolves it
> to `visible|disabled|hidden` before the page reaches the client. Malformed or embedded forms
> (`"state-{{x}}"`, `"{{ x }}"`, a bare enum typo) are rejected at **push**, and `footer` is 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, use `submitOnChange` plus a
> 200 `changes` response.
>
> ⚠️ A `label` value may **never** be the empty string (nor an `image` an empty src) — the renderer
> rejects both. Use a non-breaking space for a blank-looking label. For an image use a **fetchable**
> `http(s)` placeholder URL (or an actor-attached asset): `image.value` is loaded through the
> `/api/1.0/image?src=` proxy, which rejects a `data:` URI with `400 "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
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 |
| `$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 |