System overview Using the editor The Exotool language HTML templates The diagram The element catalogue

Exotool diagram element types

Catalog of element classnames observed in real diagrams, with their fields. The Exotool diagram is a nested tree: viewdiagram (root) → elements.pool1 (viewpool) → pages/popups/panels/connectors. The MCP exotool_save validator only checks the root classname + root metadata; the real vocabulary is enforced downstream by the cloud compiler, so a bad classname surfaces as a compiler_error, not a validation error.

Sources: an initial seed project and a multi-element example diagram (read 2026-06-28), plus the family-by-family verification below.

Verification status

As of 2026-06-29 the element families below are VERIFIED from real editor-created / sample diagrams (each section carries a "VERIFIED" banner naming its source diagram), not merely derived from the compiler source:

  • Common envelope; structural / nav / wiring; sheets (section + cross-sheet symlink pairs)
  • Forms (form2 + inputs, buttons, smart forms login_form3 / credit_card_form1)
  • Media (static_image2, static_swiper_slider, data_swiper_slider)
  • Advanced UI (floating_action_button, speed_dial, morph_popover, popover2)
  • Lists (data_list2, custom_data_list, accordion_list, swipeouts, swipeout_link, item_title)
  • BPM flow nodes (start/end/err/fork/decision/signal/listen/ repeat/subView/client_call/client_activity/lib)

Also verified: googlemap (from maptest8) and annotation (from split_dashboard_app_example / split_window).

Still source-only (in the compiler palette but not yet round-tripped): the form url/password/phone/date/datetime variants (share the form_text_input shape). form_buttons_row is source-only and to be avoided. Excluded — in development, do not use: message_connector and testcontentblock.

Common fields

Most visible elements share this envelope:

  • classname — the element type (required)
  • identifier — stable id used by the compiler/router (e.g. home, link2524)
  • title — display label / content
  • ordering — sibling sort order
  • width, height, x, y — canvas geometry
  • background — usually "white"
  • action — { "classname": "view_action" }
  • presets — selects a preset: a hash preset id (e.g. "5839c6b197569") or "other" (no preset). See Presets
  • extra_javascript, extra_css, extra_css2, extra_php — per-element code slots (note: extra_php is the 5th slot, present on newer elements)
  • template, wrapper_template — Handlebars-style render templates
  • elements — child map (or [] when none)
  • hooks — usually []
  • _params — editor field-control metadata. Either bare true flags (older seed) or rich descriptors { type, title, name, ... } where type ∈ text | dropdown | popup_text, plus is_preset_control, check_for_declarations, contains_sourcecode, driven_by_preset. Parameters driven by the selected preset are omitted from _params — see Presets.

Containment rules

Nesting is constrained — not every element may be a direct child of every other. The compiler does NOT enforce these (a bad nesting still saves/compiles "OK"), but the result renders nothing useful, so they must be respected by hand.

  • link / back_link are leaf elements and may NOT be direct children of a page, panel, or popup. A link must live inside a link-holding container: a navbar, a bottom_toolbar, or a linkmenu. To put a link on a page/panel/ popup body, wrap it in a linkmenu (a body-type container) and nest the link there. (This applies to a link used as a sequence_connector source too — the link still has to be inside a container.)
  • Direct children of a page / panel / popup must be pagebody-type elements — i.e. the body/toolbar/navbar containers and content blocks (navbar, bottom_toolbar, linkmenu, contentblock, arg_placeholder, data lists, forms, etc.), NOT bare leaves like link.
  • Containers carry a group_key indicating their slot: "navbar", "toolbar", or "body".

Sheets (the section field)

A large/cluttered diagram can be split across multiple sheets. The editor shows ONE sheet at a time and lets you switch between them. Sheets are encoded by an integer section field on each top-level element:

  • section absent (or 0) → the main sheet (sheet #0).
  • section: 1, 2, … → that sheet number.

Structural facts:

  • Moving an element to another sheet does NOT move it in the tree — every element stays in the single pool1.elements map; section is just a per-element tag.
  • Each sheet is its own canvas with independent x/y coordinates (an element on sheet 1 and one on sheet 0 can share coordinates without visually colliding). Apply the no-overlap layout rule per sheet.

Cross-sheet connections use a symlink pair (you cannot draw a sequence_connector directly between sheets, since only one sheet is visible at a time). The pattern is the same teleport as within-sheet, with each badge tagged to its own sheet:

  • The source-side symlink carries the same section as the source and receives the connector: source → Ⓧ.
  • The target-side symlink carries the same section as the target and connects onward: Ⓧ → target.
  • The two symlinks share the same title (e.g. "B"↔"B"), which is what bridges the sheets.

Example (from showcase_with_sheets, panel_left moved to sheet 1):

main sheet:  link_bt ──► symlink "B" (no section)
sheet 1:     symlink "B" (section 1) ──► panel_left (section 1)

The within-sheet "A" pair is just the special case where both badges are section 0.

Sheets also bound call scope. Beyond organizing the canvas, a sheet acts as a call-scope boundary for activity calls: when a client_call targets an activity on another sheet, that target's sheet plays the same role a subView does in scoping the call. See Calls, returns & scopes.

Placeholders & element reuse

VERIFIED (2026-06-29) — from the official samples RetailStore, AreYouAvailable, styletests (12 reuse connectors across them). Textbook case: in RetailStore a single pool-level contentblock titled "Summary" is reused via a placeholder on two different pages ("page7" and "Buy Form").

A placeholder (arg_placeholder, and the typed variants pagebody_placeholder / link_placeholder / navbar_placeholder / bottom_toolbar_placeholder / floating_action_button_placeholder) marks a slot in a container. It does two jobs:

  1. Empty slot. An unfilled placeholder is just an empty position — e.g. a fresh page ships with placeholders for its navbar, one body element, and the toolbar (rendered in the editor as dotted-outline boxes). Titles like "Tool Bar", "+Page Body", "Nav Bar", "+Input" name the slot; group_key gives its kind (toolbar/navbar/body/inputs/…).
  2. Element reuse. A placeholder wired by a sequence_connector to another element renders that element in its place. This lets one element appear in many places without duplication.

The reuse binding: a sequence_connector whose from = the placeholder's key and to = the target element's key. (This overloads sequence_connector — the same edge type used for link → page navigation also expresses placeholder → element reuse, so a connector's to may be a content element such as a contentblock or navbar, not only a screen.)

The target element can live ANYWHERE — the placeholder references it by key regardless of location (all seen in official samples): - at pool level (a sibling of the pages — a natural home for an element that doesn't belong to any single page; e.g. RetailStore's shared "Summary" contentblock), or - nested inside another container — not only a page but any element, e.g. in AreYouAvailable a placeholder on one panel reuses a navbar that lives inside a different panel.

A placeholder may also point at a symlink rather than directly at the target — composing reuse with the symlink teleport: placeholder → Ⓧ … Ⓧ → element. (Common in styletests, where placeholders route to same-titled symlinks.)

page page7:    arg_placeholder "Summary" ─┐
page Buy Form: arg_placeholder "Summary" ─┴► contentblock "Summary"   (pool level; reused on two pages)
panel "menu":  arg_placeholder "Nav Bar"  ─► navbar                   (nested inside another panel)
page Home:     arg_placeholder "A" ─► symlink "A" … symlink "A" ─► (the real target)

Authoring note (MCP vs. editor). Through this JSON/MCP interface you create placeholders and their reuse connectors directly. A human user in the editor does not create placeholders by hand — they appear automatically as the empty dotted-outline slots of a new container; reuse is created by dragging a sub-element out to pool level, which leaves a placeholder behind already connected to it; and a placeholder is filled/replaced by dragging an element onto it.

Presets

VERIFIED from the official sample button_grid_example and the preset API.

A preset is a named bundle of parameter values for a given element class. Every element carries a presets field selecting one — either a preset id (a hash, e.g. "5a976ea56244b") or "other" meaning "no preset / custom values". (This is why presets shows up on nearly every element in the catalog.)

Driven parameters. In an element's _params, the preset control is the entry with is_preset_control: true (its control_key is normally presets). Any parameter whose descriptor has driven_by_preset: "presets" takes its value from the selected preset. Once a parameter is driven by the preset it disappears from the element's parameter menu — i.e. it is removed from _params, leaving only the preset control and the remaining non-driven parameters. (Confirmed in button_grid_example: the two grid linkmenus select the linkmenu "Grid" preset 5a976ea56244b, and their _params contain only the preset control — the preset-driven template/wrapper_template are gone, supplied by the preset. Its links use preset 61586aea541a1, and a client_activity uses 6158649edf385.)

Storage shape & tools. Presets are stored per project (NOT in the diagram), and are now exposed by the MCP tools exotool_list_presets and exotool_store_preset (backed by the cloud services edapi_load_presets / edapi_store_preset):

  • Organized as view_class → control_key (presets) → preset_id (hash) → { title, values, level } — e.g. a linkmenu preset { "title": "Grid", "values": { "template": "…", "wrapper_template": "…" }, "level": "system" }. level ∈ system/user/project/diagram.
  • exotool_list_presets { project?, view_class? } → an outline of all classes, or (with view_class) that class's presets with their full values.
  • exotool_store_preset { operation, view_class, where?, title?, values?, preset_id?, project?, control_key? } → returns the preset id. operation ∈ create / store / delete (NOT "write"): create makes a new preset (the server generates the id), store overwrites an existing preset_id, delete removes it. where ∈ user (default) / system (admin only) / project / diagram; preset_id is required for store/delete, ignored for create.
  • To copy a preset into a new user preset: read a source preset's values with exotool_list_presets(view_class=…), then exotool_store_preset with operation:"create", where:"user", a new title, and those values.

Authoring via MCP: set an element's presets field to an existing preset id (or "other"), and create/copy/delete presets with exotool_store_preset.

Type catalog

Structural / root

  • viewdiagram — root. Carries internal_idctr, static_assets, translated_code, user_var_names (ensured by DiagramModel).
  • viewpool — the app container. Holds theme/UI fields (ui_style, dark_theme, background, transparent_popups, …) and all pages/popups/panels. The app icon and app background image are project assets, not diagram fields — set them with exotool_upload_app_icon / exotool_upload_app_background (no exotool_save needed; see SKILL.md "Asset uploads").
  • view_action — the action sub-object on most elements (not a standalone node).

Screens / containers (live in the pool)

  • page — a screen/route. Has hide_on_scroll; children = navbar/toolbar/body.
  • popup — a popup/modal screen; valid to target of a connector.
  • panel — a side drawer. Extra fields: side (left|right) and cover (cover|…, the open effect). Holds the same child kinds as a page. Targeted by sequence_connectors.

Within-screen containers

  • navbar — top navigation bar (group_key: "navbar"); holds link/back_link.
  • bottom_toolbar — bottom bar (group_key: "toolbar"); holds links (often icon links like <i class="f7-icons">home</i>).
  • linkmenu — list-style menu (group_key: "body"); template iterates {{#each links}} over child links; renders a Framework7 list-block.
  • arg_placeholder — a slot in a container (e.g. "Tool Bar", "+Page Body"); either an empty position or, when wired by a connector to another element, a reuse reference to it. Minimal _params (identifier only). See Placeholders & element reuse.
  • contentblock — a block of HTML/template content; template + wrapper_template with {{#each subs}}.
  • link — nav/action link. Fields: position (left|right), auto_close, do_on_action (source code). Wired to a target by a sequence_connector.
  • back_link — "go back" link (icon template); position: "left".

Annotation

  • annotation — a free-floating note/comment box on the canvas. VERIFIED from the official samples split_dashboard_app_example / split_window. Minimal: note (the text, e.g. "NOTE: Admin Side Panel has some custom CSS in CSS1"), geometry (x/y/width/height), ordering, and background (a translucent fill, e.g. "rgba(255,255,255,.1)"). NO identifier/_params/action/elements. Purely documentation for the diagram author — it has no effect on the compiled app. (Editor/diagram-only; not in the compiler's f7_* palette.)

Wiring (live in the pool, siblings of pages)

  • sequence_connector — directed edge. from = source element key (a link, back_link, symlink, …), to = target screen key (page/popup/panel). condition (e.g. "always"), line_path = routed points. Each "always" connector also gets a root translated_code[<connKey>] = { condition: false } entry.
  • symlink — a labeled jump-point node (VISIBLE on canvas, 35×35). Minimal: only classname, title, ordering, width/height/x/y — NO from/to/identifier/action/elements/_params. Used in matched pairs sharing the same title (e.g. two "A" boxes at opposite ends) so a connector can "teleport" across the diagram instead of drawing one long line. A symlink can be the from of a sequence_connector. In the editor it renders as a small circular labeled badge (e.g. Ⓐ), not a rectangular element box — that round badge is the visual giveaway for spotting a symlink on the canvas. BOTH ends of the pair MUST be wired or the symlink does nothing: a source connects into one badge (source → Ⓐ) and the matching-label badge connects out to the real target (Ⓐ → target), so the pair reads as source → Ⓐ … Ⓐ → target ("source teleports to target"). A badge with nothing feeding it (or nothing leaving it) is a dangling half-pair representing no jump — a common authoring mistake. Pair the two symlinks by giving them the same title. A symlink pair also serves as the cross-sheet connection mechanism (see Sheets below): when source and target are on different sheets, each badge sits on its own sheet and the shared title bridges them.

Forms — the form2 family

VERIFIED (2026-06-29). form2, form_title, form_text_input, form_email_input, form_textarea_input, form_button2 appear in the sample projects email_test / RetailStore / radio_example. The form_dropdown_input / form_toggle_input / form_slider_input variants (incl. the slider's min/max/start/step) were confirmed against an editor-created form; they share the form_text_input shape, as do the url/password/phone/date/datetime variants. Smart forms (login_form3, credit_card_form1) are verified from RetailStore / Magazine. form_buttons_row is source-only (f7types_forms.php) and is to be avoided (see caveats).

form2 — the form container. It is a pagebody element, so it sits in a page/panel/popup body (group_key: "body"), like a contentblock. Notable points:

  • Its children (elements) are the input rows, and each child carries group_key: "inputs" (NOT "body") — that is the form2's input slot. A +Input arg_placeholder (also group_key: "inputs") marks where new rows go.
  • The form's identity is its identifier; activity code reads/writes its values with get_form_values(<identifier>) / set_form_values(...), and forms with the same key stay in sync. NOTE: there is no stored form_key field/param on the element — it is calculated at compile time, so don't add one.
  • _params: identifier, title, template, wrapper_template, presets, extra_javascript, extra_css, extra_css2, extra_php. Renders as a Framework7 list-block inputs-list.

Input rows (all live inside the form2 with group_key: "inputs"). They share a common field set, verified identical across the official samples: the row's title is its key/name, label and placeholder default to the literal string "(automatic)" (derived from the key at runtime), icon (always "" in the samples — see caveat below), plus template/wrapper_template/presets/extra_*. Their _params mark title with {title:"Key"} and icon as a dropdown.

Schema-version note. The official samples are an older form schema: their input rows, form_title, and form_button2 carry no do_on_action field. A form built in the current editor adds a do_on_action code hook to these — so treat do_on_action as a newer, current-editor param that the older samples predate.

classname Role HTML input
form_title a section heading inside the form (title + template/wrapper_template; no label/placeholder/icon) (text only)
form_text_input base text field type=text
form_email_input email field (validates e-mail) type=email
form_url_input URL field (validates URL) type=url
form_password_input password field (validates strength) type=password
form_phone_input phone field (validates phone) type=tel
form_dropdown_input a <select>; edit the template for the options <select>
form_date_input date picker type=date
form_datetime_input local date/time picker type=datetime-local
form_toggle_input on/off switch type=checkbox
form_slider_input range slider; extra params min/max/start/step (string values, defaults "0"/"100"/"50"/"0.1") type=range
form_textarea_input resizable multi-line text <textarea>

Validation is driven by the input type plus a required flag; a failed field sets a <key>_error variable the form can react to.

Buttons - form_button2 — a button: title (default "Submit") + template/ wrapper_template (no label/placeholder/icon; do_on_action only on current-editor instances — see schema-version note). Clicking fires a submit action. Place it directly inside the form2 (also group_key: "inputs"). - form_buttons_row — a row meant to lay out form_button2s side-by-side. ⚠ AVOID — does not work as expected (per project guidance, 2026-06-29). Put buttons directly in the form2 instead.

Smart / pre-built forms (form_with_action and subclasses: login_form3, credit_card_form1) — VERIFIED, shapes read directly from the official samples RetailStore and Magazine (both contain a login_form3 and a credit_card_form1). Unlike a plain form2, a smart form is a self-contained leaf element:

  • elements: [] — it does NOT hold form_*_input children. The entire form (all the fields plus its Submit/Cancel buttons) is baked into its big wrapper_template, and the submit logic lives in do_on_action (real Exotool activity code that runs on submit, after the form's values are collected into a _…values_ variable). It sits in a page body (group_key: "body").
  • _params (exactly, in both samples): identifier, title, info (the help text shown on the form), wrapper_template, do_on_action, presets, extra_javascript, extra_css, extra_css2, extra_php. No others.
  • login_form3 — its do_on_action handles submit / register / sel_sign_up / sel_log_in / sel_forgot_password actions via login_session(...) / register_user(...) / get_current_session(). (Some non-sample instances also carry an email_usernames dropdown param, but neither official sample's login_form3 has it — treat it as optional, not standard.)
  • credit_card_form1's do_on_action calls charge_credit_card(...).

Prefer these over the deprecated _login_form / login_form2 / _form. To customize a smart form you edit its wrapper_template (the fields) and do_on_action (the logic) — you don't add child input rows.

⚠ Form authoring caveats (project guidance, 2026-06-29)

  • Always set icon to "none" (icon: ""). The form icons do not render properly, so leave the icon empty on every input that has an icon param. The editor's default is already ""; never set it to an icon-form-* class.
  • Do not use form_buttons_row — it does not behave correctly. Put form_button2s directly inside the form2.

Verified shape

form2 on a page body, holding a text input, a slider, and a button — from a current-editor form, so the children include do_on_action (the older official samples omit it; see the schema-version note above). Trimmed — every child also has template/wrapper_template/presets/extra_* and a full _params map; the _params use the rich {type,title,name} descriptor form:

"form2_diag472": {
  "classname": "form2", "identifier": "form2473", "title": "Form",
  "template": "\n<ul>\n\t{{#each subs}}\n\t  {{expansion}}\n\t{{/each}}\n</ul>\n",
  "wrapper_template": "\n<div class=\"{{parameters.form_key}} {{classes}}\" {{attributes}} >\n <div class=\"list-block inputs-list\"  >\n    <ul>\n\t{{#each subs}}\n\t  {{expansion}}\n\t{{/each}}\n     </ul>\n  </div>\n</div>\n",
  "presets": "other", "extra_javascript": "", "extra_css": "", "extra_css2": "", "extra_php": "",
  "ordering": 3, "group_key": "body", "width": 115, "height": 270, "y": 92, "x": 9,
  "action": { "classname": "view_action" }, "background": "white", "hooks": [],
  "elements": {
    "form_text_input_diag1338": {
      "classname": "form_text_input", "identifier": "form_text_input1339",
      "title": "Name", "label": "(automatic)", "placeholder": "(automatic)",
      "icon": "", "do_on_action": "",
      "ordering": 1, "group_key": "inputs", "width": 105, "height": 12, "y": 51, "x": 5,
      "action": { "classname": "view_action" }, "background": "white", "elements": [], "hooks": []
    },
    "form_slider_input_diag2142": {
      "classname": "form_slider_input", "identifier": "form_slider_input2143",
      "title": "Slider", "label": "(automatic)", "placeholder": "(automatic)", "icon": "",
      "min": "0", "max": "100", "start": "50", "step": "0.1", "do_on_action": "",
      "ordering": 5, "group_key": "inputs", "width": 105, "height": 12, "y": 167, "x": 5,
      "action": { "classname": "view_action" }, "background": "white", "elements": [], "hooks": []
    },
    "form_button2_diag890": {
      "classname": "form_button2", "identifier": "form_button2891",
      "title": "Submit", "do_on_action": "",
      "ordering": 7, "group_key": "inputs", "width": 105, "height": 12, "y": 225, "x": 5,
      "action": { "classname": "view_action" }, "background": "white", "elements": [], "hooks": []
    },
    "arg_placeholder_diag1027": {
      "classname": "arg_placeholder", "identifier": "arg_placeholder1028",
      "group_key": "inputs", "title": "+Input", "ordering": 8,
      "width": 111, "height": 17, "y": 254, "x": 5,
      "action": { "classname": "view_action" }, "background": "white",
      "elements": [], "hooks": [], "_params": { "identifier": [] }
    }
  },
  "_params": { /* identifier, title, template, wrapper_template, presets, extra_* — NO form_key */ }
}

Media — images & swipers

VERIFIED (2026-06-29) — element shapes read directly from the official sample projects: static_swiper_slider from Magazine / layers, data_swiper_slider from RetailStore. static_image2 does not appear in any official sample (the samples use legacy static_image, including as swiper slides), so its shape is confirmed only from non-sample diagrams. All are pagebody elements (group_key: "body").

static_image2 — a single static image (a leaf, elements: []). - The image is uploaded in the editor (or, via MCP, with exotool_upload_image — store it under a name, then point the template at img/<name>.jpg); the element's template holds the image path (e.g. "img/{{permID}}.jpg"), and image_uploaded: true marks that an asset is present. - Distinctive params: background_image_upload_button (_params type image_asset, with a filename) is the editor's upload control; image_icon (_params type html_div); plus the usual title/template/wrapper_template/ presets/extra_*.

static_swiper_slider — a carousel of a fixed set of images (a container). - Children are the slides — static_image elements (in the official samples these carry no group_key); use static_image2 for new work. - Swiper params: effect (transition, e.g. "slide" / "coverflow" / "cube"), speed (e.g. 400–1400), space_between, autoplay (1), autoplay_delay (3000), plus do_on_entry / do_on_action, template, wrapper_template.

data_swiper_slider — a data-driven carousel that loops over an array (a container that repeats one child per item). - ar_reference names the array/set to loop over — a full Exotool expression (e.g. ordered(cloudobjects_of_type("item"),"price")); populate_on controls when it refreshes (e.g. "page_refresh"). - Its single child is the per-item template, marked with group_key: "item_content" (e.g. a contentblock/static_image whose template references the item's fields). This item_content slot is the repeater pattern: the child is rendered once per array element. - Shares the swiper params (effect/speed/space_between/autoplay/ autoplay_delay, do_on_entry/do_on_action) but has no top-level template (the repeated child is the template).

Key distinction: static_swiper_slider = fixed slides you add by hand; data_swiper_slider = one item_content child repeated over an ar_reference array. The same item_content repeater idea applies to the other data-driven list types (data_list2, custom_data_list).

Advanced UI — FAB, speed dial, popovers

VERIFIED (2026-06-29) — shapes read directly from the official sample projects: morph_popover from styletests, popover2 from popover_and_hidescroll_example (both confirmed verbatim, no missed params). floating_action_button and speed_dial do not appear in any official sample, so their shapes are confirmed only from non-sample diagrams. Three of the four sit in a page's toolbar slot (group_key: "toolbar"); popover2 sits in the body. Note these are older elements — several carry the lighter param set (extra_javascript/extra_css only, no extra_css2/extra_php).

floating_action_button (FAB) — a button that floats over the page (group_key: "toolbar"). It holds link children which are the action(s) it triggers. Params: identifier, title, template, presets, extra_javascript, extra_css.

speed_dial — a FAB that expands into several actions (group_key: "toolbar"). In the sample it is a leaf (elements: []) with the actions baked into its template. Params: identifier, title, template, presets, extra_* (incl. extra_css2/extra_php).

morph_popover — a popover that "morphs" out of a button (group_key: "toolbar"). Its content goes in a child pagebody_placeholder (the body slot) alongside a +Body arg_placeholder. Params: identifier, title, template, presets, extra_javascript, extra_css.

popover2 — a button that opens a popover bubble (group_key: "body"). Its wrapper_template renders the trigger button (<a data-popover …>); the popover's content is the element's child body (e.g. a contentblock + a +Body arg_placeholder). Params: identifier, title, template, wrapper_template, presets, extra_javascript, extra_css.

Quick contrast: FAB holds link actions; speed dial bakes its actions into its template; morph_popover and popover2 are both popovers holding body content — morph_popover via a pagebody_placeholder in the toolbar, popover2 as a body-slot button whose children are the popover contents.

Lists & list items

VERIFIED (2026-06-29) — shapes read directly from the official sample projects (confirmed verbatim, no corrections): data_list2 + item_title + swipeouts + swipeout_link from picture_database, custom_data_list from custom_data_list_example, accordion_list from Icons. All are pagebody elements (group_key: "body").

Data-driven lists — data_list2 and custom_data_list

Both loop over a data source and render one item per element, using the same ar_reference + populate_on repeater pattern as data_swiper_slider:

  • ar_reference — the array/set to loop over, written as an Exotool expression (e.g. cloudobjects_of_type("storedCloudImage"), or a variable like ar). Inside the per-item template you reference the item's own fields with {{field}} / {{@key}}.
  • populate_on — when the list refreshes (e.g. "page_refresh").
  • Plus do_on_entry / do_on_action, wrapper_template, presets, extra_*.

The difference is the item template:

  • data_list2 uses structured per-item slots — its children carry distinct group_keys that map to parts of a list item:
  • item_title (group_key: "item_title") — the item's title line; its template references item fields (e.g. {{original_filename}}).
  • per-item content such as a static_image/contentblock (group_key: "item_content").
  • swipeouts (group_key: "swipeouts") — the swipe-action drawer (below).
  • custom_data_list uses a free-form item layout — its children are ordinary body elements (a contentblock, a styled link, etc.) that you arrange yourself, instead of the fixed item_title/item_content/swipeouts slots. Use it when you want full control over each row's markup.

A list item's swipe drawer:

  • swipeouts — the container (group_key: "swipeouts" inside a data_list2); renders swipeout-actions-right and holds swipeout_links.
  • swipeout_link — one swipe action. Distinctive params: swipe_action (e.g. "delete") and color (e.g. "bg-red"), plus a do_on_action hook, wrapper_template, presets, extra_*.

item_title

A list item's title line (lives inside a data_list2, group_key: "item_title"). Just title + a template that pulls item fields, plus wrapper_template/ presets/extra_*. Renders a Framework7 item-title.

accordion_list

A collapsible list — NOT inherently data-driven (no ar_reference). It holds free-form body content (contentblocks, an +Body arg_placeholder; group_key: "body"). Distinctive params: is_inset ("yes"/"no") and is_custom ("yes"/"no"); plus title/presets/extra_*.

Summary of the repeater family: data_list2 (structured item slots), custom_data_list (free-form items), data_swiper_slider (carousel), and googlemap (map, below) all share ar_reference + populate_on. accordion_list is a static collapsible container, and the legacy data_list is deprecated → use data_list2.

googlemap

A data-driven Google Map (a body leaf, elements: []). VERIFIED from the official sample maptest8. It is part of the repeater family: ar_reference is the set of markers to plot (e.g. cloudobjects_of_type("marker")) and populate_on controls refresh (e.g. "page_refresh"), alongside do_on_entry/do_on_action, wrapper_template (renders a content-block map with a map_in div), presets, extra_*. group_key: "body".

BPM flow nodes (verified shapes)

VERIFIED (2026-06-29) — shapes read directly from the official sample projects: fork (Fork_test), decision (RetailStore), signal / listen (AreYouAvailable, SignalAndListen, OtherAppSend), repeat (OtherAppSend), client_activity (most samples). start, end, err, subView, client_call, and lib do not appear in any official sample, so their shapes (incl. subView's covered_elements) are confirmed only from non-sample diagrams. This is the field-level companion to Activities & behavior, which covers the concepts.

These are the behavior/flow layer, not UI. They live at pool level (siblings of pages, like sequence_connectors), are leaf nodes (elements: [], small square boxes ~37–56px), and are wired into a flow graph by sequence_connectors (start → … → end/err). They carry the standard envelope (identifier, title, presets, extra_*, action, hooks) and may carry a section (sheet). Where an activity holds logic, it is in a code field (Exotool activity language — see Activities & behavior).

classname Role Distinctive fields beyond the envelope
start flow entry point — (none)
end normal flow terminator —
err error terminator —
fork split into parallel branches (branches = outgoing connectors) —
decision conditional branch — branches are its outgoing connectors' conditions — (the official-sample decision carries only the standard envelope; no inline code. A code field appears on some non-sample instances but is not standard.)
client_activity the core activity node; runs Exotool code (modern — activity is deprecated) code (the activity source)
client_call call a subroutine / sub-process code
signal send a push signal on a channel channel_key (e.g. "default"), code (default send_signal({{identifier}},{{parameters.channel_key}}))
listen receive signals on a channel channel_key, do_on_connect, do_on_disconnect, do_on_message, do_on_entry
repeat timer loop cycle_time (ms, e.g. 30000), do_on_cycle (code run each cycle)
subView sub-process grouping box; run via run_subprocess(...) covered_elements — a map of the element-keys it visually groups (NOT nested elements)
lib external code-library reference library_type (e.g. "php_file"), file_name, theFile, repository_url, error_html

Notable details:

  • code is the activity-logic field for client_activity / client_call / signal (and repeat uses do_on_cycle). This is where the location-transparent Exotool language lives. A decision carries no inline code in the official sample — it branches on its outgoing connectors' conditions.
  • subView is special — it does NOT nest its members under elements; it references them by key in a covered_elements map (a visual region that "covers" the activities/pages it groups). Invoke it with run_subprocess(...). Its geometry is SEMANTIC, not cosmetic: a subView covers (scopes) any element whose bounding box it fully encloses graphically — so covered_elements is effectively "what the box encloses." When authoring via JSON, size/position it deliberately and make sure it doesn't accidentally enclose a neighbour (the one element whose x/y/size changes behaviour, not just looks).
  • signal/listen pair on a channel_key — signal sends (send_signal), listen receives, with connect/disconnect/message/entry hooks.

Calls, returns & scopes

Source: the platform's call-activities specification + non-sample call/subView diagrams. The call-machinery element types here (client_call, subView, end, err, error_hook, start, lib) appear in no official sample, so this subsection is doc-sourced, not sample-verified.

A client_call invokes a target activity; its hooks array lists attached nodes (a symlink to the target, an error_hook, …). How a call returns:

  • A call returns only when it reaches an end, an err, or hits a scope violation. Internal errors do NOT make a call return or fail.
  • end — returns from the most recent call; execution continues after the call element.
  • err — runs the error hooks attached to the call activity, then ends the call.

error_hook — attaches to a client_call and fires when an err is encountered during that call; execution then continues wherever the hook leads. It is a small node whose action carries the wiring: { "classname": "error_hook_action", "error_code": "default", "hook_target": "<client_call key>" } (no identifier/_params).

subView = call scope. The scope of a call is the sheet the target sits on plus the subViews that cover it (its covered_elements). Elements in the same sheet covered by the same subViews are in-scope; nested subViews give the "multiple sublevels" of calling. A call can also target an activity on a different sheet — in that case the target's sheet acts as a scope boundary exactly like a subView (the sheet plays the subView's role). A scope violation = a page or popup opening outside the current call scope — it acts like hitting an err. Activities never cause scope violations; only page/popup elements do.

Not implemented yet: hooks on subViews, and timer hooks (per the call-activities specification). Treat both as unavailable.

api — published API endpoint (VERIFIED)

VERIFIED (2026-06-29) — from the sample project TR1_api: the api elements in diagrams/app.diagram and the generated backend/backend.php (API_create_beneficiary, API_create_new_broker). Implemented as a subclass of client_activity.

An api node publishes a server-side, externally-callable API endpoint built from an activity flow. It is a pool-level activity node (the editor shows it as a stylized >>, ~40×31), a sibling of pages / client_activity / connectors and a leaf (elements: []). You name it and list its returned variables, then wire its single outgoing sequence_connector into the client_activity that does the work (the node's own code is usually left empty in that case).

Fields (beyond the standard envelope):

field meaning
identifier / title the endpoint name — compiles to an API_<identifier> handler (e.g. create_beneficiary → API_create_beneficiary)
code optional inline activity source; usually empty when the logic lives in the connected activity
output_vars comma-separated names returned to the caller (e.g. "userInfoID,profileID")
extra_php / extra_javascript / extra_css* the usual escape hatches

multiple_output_links_allowed is false — exactly one outgoing connector.

What it compiles to. The compiler collects the node's code plus the code of the activity flow reachable from its output, recursively (following out-transitions; conditional branches become if / else-if from the connector conditions), wraps the whole body in require_on_server() (so the endpoint runs server-side), and emits a class API_<identifier> extends generatedCodeRoot into the app's backend.php. Input args are inferred as the free variables (referenced but never assigned in the body); the response carries the output_vars. The compiler errors if the output leads to a non-activity or the walk loops back on itself.

Auto-generated documentation + call stubs. Above each generated class, backend.php documents the endpoint: the collected source, the Args passed from client, the Result values returned to client, and ready-to-paste JavaScript and Python call stubs.

Call protocol. Callers form-POST to the app entrypoint with protocol=jsonp_dgapp_responder, service=API_<identifier>, and args=<json> (a JSON object of the inputs); the response is JSON of the output_vars. From the app's own JS it is wrapped as DGcloud.callApi('API_<identifier>', {args}).then(returnvalues => …). (This jsonp_dgapp_responder runtime protocol is distinct from the editor's jsonp_responder_1 and the file-upload responder.)

Worked example diagram

An example diagram as rendered in the Exotool editor — a single diagram exercising most of the catalog in one place. Reading it left→right:

  • A circular Ⓐ badge (a symlink) on the far left wires into the Home page. Home contains a contentblock and a linkmenu ("Link Menu") holding four child links.
  • The "page with navbar" page shows a top navbar (a back_link labeled "Back") and a bottom bottom_toolbar (icon links "home" / "Link").
  • Home's link-menu links fan out to the right, arriving at a second Ⓐ symlink (the matched pair — same label, the teleport target) and at the containers beyond: a left panel, a right panel (each with Nav/Tool bars, Content, +Page Body slots), and a popup (with a "close" link).

This is the reference image to compare against when reasoning about how the JSON geometry/wiring maps to the visible canvas, and the clearest illustration of the symlink Ⓐ↔Ⓐ teleport-pair mechanism.

Example shapes

"symlink_diag10771": {
  "classname": "symlink", "title": "A", "ordering": 3,
  "width": 35, "height": 35, "y": 153.5, "x": 3
},
"sequence_connector_diag10780": {
  "classname": "sequence_connector", "condition": "always",
  "from": "symlink_diag10771", "to": "page_diag2352",
  "line_path": { "n": 4, "points": { ... } }
}

A second symlink titled "A" sits at the far side (x: 455) as the pair.

panel

"panel_diag815": {
  "classname": "panel", "identifier": "panel816", "title": "left panel",
  "side": "left", "cover": "cover", "presets": "other",
  "ordering": 7, "width": 63, "height": 132, "y": 26, "x": 512,
  "action": { "classname": "view_action" }, "background": "white",
  "elements": { /* arg_placeholder (toolbar), contentblock, arg_placeholder (body) */ }
}

Right-side variant: "side": "right".

bottom_toolbar

"bottom_toolbar_diag6681": {
  "classname": "bottom_toolbar", "identifier": "bottom_toolbar6682",
  "group_key": "toolbar", "title": " ", "ordering": 1,
  "width": 110, "height": 27, "y": 120, "x": 9,
  "action": { "classname": "view_action" }, "background": "white",
  "elements": { /* icon links, e.g. <i class="f7-icons">home</i> */ }
}

linkmenu

"linkmenu_diag2478": {
  "classname": "linkmenu", "identifier": "linkmenu2479", "title": "Link Menu",
  "template": "<div class=\"list-block\">{{#each links}}<a ...>{{title}}</a>{{/each}}</div>",
  "wrapper_template": "<div class=\"{{classes}}\" {{attributes}}>{{_content_}}</div>",
  "group_key": "body", "ordering": 3, "width": 93, "height": 118, "y": 99, "x": 9,
  "elements": { /* child link elements */ }
}

Complete compiler palette (authoritative)

Source of truth: the compiler's own type palette. A few classnames are special-cased; otherwise a classname compiles iff the compiler defines a type of that exact name. Unknown classnames are collected as unknown_types (compile error). This makes the list below complete as of 2026-06-28.

Special-cased by the compiler's loader: - link → aliased to the pagelink class. - sequence_connector → no object; wiring handled separately. - symlink → generic has_params object (no dedicated class — matches the minimal node shape documented above). - message_connector → instantiated only if its class exists. - error_hook, timer_hook → hook attachment points.

Containers & infrastructure (f7_compiler.php): viewdiagram, viewpool, page, popup, panel, navbar, bottom_toolbar, pagelink (the class behind link), back_link, pagebody, linkmenu, popover2, floating_action_button, speed_dial, morph_popover, message_connector, client_activity.

Page-body content (f7types.php) — list types detailed in Lists & list items above: contentblock, accordion_list, item_title, swipeouts, swipeout_link, data_list2, custom_data_list, googlemap (data-driven map — see googlemap). (testcontentblock is in development — do not use.)

Activity / process-flow (f7types.php) — BPM-style flow nodes, detailed in BPM flow nodes above (plus client_activity from f7_compiler.php): start, end, err, fork, decision, signal, listen, repeat, subView, client_call, lib.

Forms & inputs (f7types_forms.php) — field detail in Forms — the form2 family above: form2 (container), form_input (base), form_title, form_text_input, form_email_input, form_url_input, form_password_input, form_phone_input, form_dropdown_input, form_date_input, form_datetime_input, form_toggle_input, form_slider_input, form_textarea_input, form_buttons_row, form_button2, form_with_action, credit_card_form1, login_form3.

Images & media (f7types_images.php, f7types_swiperslider.php) — detail in Media — images & swipers above: static_image2, data_swiper_slider, static_swiper_slider.

API / integration (f7types_api_in.php): api — a pool-level node that publishes a server-side API endpoint from an activity flow; see api — published API endpoint.

Placeholders (f7_compiler.php) — container slots / reuse references (see Placeholders & element reuse), the family that arg_placeholder belongs to: arg_placeholder, pagebody_placeholder, link_placeholder, navbar_placeholder, bottom_toolbar_placeholder, floating_action_button_placeholder.

Diagram-only / wiring (handled by the loader, not page content): sequence_connector, symlink, view_action, error_hook, timer_hook, annotation (a canvas note/comment — see Annotation).

Activities & behavior (how the flow types work)

Source: the overview (§4–6) and the language reference (exotool_docs("overview"), exotool_docs("language")). The process-flow element types above are not UI — they are the behavior layer.

What an activity is. An activity holds a snippet of the Exotool language (see the language reference (exotool_docs("language"))) — assignments, if/else, function calls, always, etc. You don't say where it runs: location transparency means the compiler decides which parts become client JavaScript and which become server PHP, and generates the device↔server communication itself. client_activity is the modern activity element (the legacy activity is deprecated).

Two ways behavior attaches: - As standalone activities wired in sequence — client_activity (and the other flow nodes) connected by sequence_connectors so steps run in order. This is the form the official samples actually use — client_activity appears in 22 of the official samples with real Exotool code. VERIFIED examples: RetailStore (the_image = imageprompt() … if (upload_error) { … } else { item = create_cloudobject("item") … }), AreYouAvailable (logout()), Fork_test (cc = datestring()). - On an element, via hooks — properties that carry activity code: do_on_action on a link/back_link (its _params.do_on_action can mark contains_sourcecode: true) and Do On Entry / do_on_entry (runs when a screen opens). These are real params the language docs describe, but none of the official samples scanned set them (behavior is done with client_activity nodes instead) — so treat inline hooks as supported-but-uncommon. The hook-attachment node types error_hook / timer_hook (from diagram_loader.php) are source-derived only — they appear in no official sample.

The flow-node vocabulary (BPM-style; defined in f7types.php): - start / end / err — flow entry, normal terminate, error terminate. - fork — split into parallel branches; decision — conditional branch. - repeat — loop a step; subView — a sub-process container (run via run_subprocess(...)). - client_call — invoke a client-side call; lib — pull in a code-library reference; client_activity — a unit of activity code. - signal (send) / listen (receive) — push-style messaging between activities or apps (send_signal(...)); listen waits for a signal.

Key language facts worth remembering when authoring/reading activity code: - No statement terminator — statements are separated by whitespace, not ;. - Binary operators have no precedence and associate right — always parenthesize (a + b * c is NOT what you'd expect). - always <assignment> is a reactive binding (re-runs when its inputs change) and is client-side only — never feed secrets into an always. - Placement overrides exist when inference is wrong: declare_location <var> client|server, if_on_client <stmt> / if_on_server <stmt>, and raw escape hatches JS< … >JS (async JS<P … >JS) and PHP< … >PHP (server scope exposed via $args, return flows back). - declare_type supports cloudobject with { … } — persistent server-stored records. Data lives as cloud objects grouped into cloud sets (queried with lookup, subset, ordered, count, …); a data_list2 binds to a set. - Compiled output contains DBsource(...) / DBpos(...) debug markers — injected by the compiler for the visual debugger; not something you write.

The language ships a built-in function library (auth, cloud data, sets, user input like prompt/imageprompt/fileprompt, modals, UI control like sync/ page_back/close_popup, messaging, mail, time, math, strings). The authoritative, current list is Exotool's generated online Function Reference; section 13 of the language reference (exotool_docs("language")) is a condensed map.

DEPRECATED — do NOT author

These are legacy types. They still compile (the classes exist) but must not be used in new diagrams; prefer the modern replacement.

Deprecated classname Use instead
data_list data_list2
_form form2
_login_form login_form3
login_form2 login_form3
activity client_activity
static_image static_image2
pagebody_old pagebody
search_input (deprecated — no direct replacement)
search_popup (deprecated — no direct replacement)
data_exclusive (deprecated exclusive gateway)
form_gender_input a form_dropdown_input (avoid hard-coded gender input)
test_onearg (dev/test only — never author)

When you encounter one of these in an existing diagram, leave it untouched unless asked to modernize; when authoring new content, pick the replacement.