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 formslogin_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 / contentordering— sibling sort orderwidth,height,x,y— canvas geometrybackground— usually"white"action—{ "classname": "view_action" }presets— selects a preset: a hash preset id (e.g."5839c6b197569") or"other"(no preset). See Presetsextra_javascript,extra_css,extra_css2,extra_php— per-element code slots (note:extra_phpis the 5th slot, present on newer elements)template,wrapper_template— Handlebars-style render templateselements— child map (or[]when none)hooks— usually[]_params— editor field-control metadata. Either baretrueflags (older seed) or rich descriptors{ type, title, name, ... }wheretype∈text | dropdown | popup_text, plusis_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_linkare leaf elements and may NOT be direct children of apage,panel, orpopup. A link must live inside a link-holding container: anavbar, abottom_toolbar, or alinkmenu. To put a link on a page/panel/ popup body, wrap it in alinkmenu(a body-type container) and nest the link there. (This applies to a link used as asequence_connectorsource too — the link still has to be inside a container.)- Direct children of a
page/panel/popupmust 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 likelink. - Containers carry a
group_keyindicating 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:
sectionabsent (or0) → 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.elementsmap;sectionis just a per-element tag. - Each sheet is its own canvas with independent
x/ycoordinates (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
symlinkcarries the samesectionas the source and receives the connector:source → Ⓧ. - The target-side
symlinkcarries the samesectionas the target and connects onward:Ⓧ → target. - The two
symlinks share the sametitle(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: inRetailStorea single pool-levelcontentblocktitled "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:
- Empty slot. An unfilled placeholder is just an empty position — e.g. a fresh
pageships with placeholders for itsnavbar, 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_keygives its kind (toolbar/navbar/body/inputs/…). - Element reuse. A placeholder wired by a
sequence_connectorto 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_exampleand 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 (withview_class) that class's presets with their fullvalues.exotool_store_preset{ operation, view_class, where?, title?, values?, preset_id?, project?, control_key? }→ returns the preset id.operation∈create/store/delete(NOT "write"):createmakes a new preset (the server generates the id),storeoverwrites an existingpreset_id,deleteremoves it.where∈user(default) /system(admin only) /project/diagram;preset_idis required forstore/delete, ignored forcreate.- To copy a preset into a new user preset: read a source preset's
valueswithexotool_list_presets(view_class=…), thenexotool_store_presetwithoperation:"create",where:"user", a newtitle, and thosevalues.
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. Carriesinternal_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 withexotool_upload_app_icon/exotool_upload_app_background(noexotool_saveneeded; see SKILL.md "Asset uploads").view_action— theactionsub-object on most elements (not a standalone node).
Screens / containers (live in the pool)
page— a screen/route. Hashide_on_scroll; children = navbar/toolbar/body.popup— a popup/modal screen; validtotarget of a connector.panel— a side drawer. Extra fields:side(left|right) andcover(cover|…, the open effect). Holds the same child kinds as a page. Targeted bysequence_connectors.
Within-screen containers
navbar— top navigation bar (group_key: "navbar"); holdslink/back_link.bottom_toolbar— bottom bar (group_key: "toolbar"); holdslinks (often icon links like<i class="f7-icons">home</i>).linkmenu— list-style menu (group_key: "body"); template iterates{{#each links}}over childlinks; renders a Framework7list-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(identifieronly). See Placeholders & element reuse.contentblock— a block of HTML/template content; template + wrapper_template with{{#each subs}}.
Links / actions
link— nav/action link. Fields:position(left|right),auto_close,do_on_action(source code). Wired to a target by asequence_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 samplessplit_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, andbackground(a translucent fill, e.g."rgba(255,255,255,.1)"). NOidentifier/_params/action/elements. Purely documentation for the diagram author — it has no effect on the compiled app. (Editor/diagram-only; not in the compiler'sf7_*palette.)
Wiring (live in the pool, siblings of pages)
sequence_connector— directed edge.from= source element key (alink,back_link,symlink, …),to= target screen key (page/popup/panel).condition(e.g."always"),line_path= routed points. Each "always" connector also gets a roottranslated_code[<connKey>] = { condition: false }entry.symlink— a labeled jump-point node (VISIBLE on canvas, 35×35). Minimal: onlyclassname,title,ordering,width/height/x/y— NOfrom/to/identifier/action/elements/_params. Used in matched pairs sharing the sametitle(e.g. two "A" boxes at opposite ends) so a connector can "teleport" across the diagram instead of drawing one long line. Asymlinkcan be thefromof asequence_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 assource → Ⓐ … Ⓐ → 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 twosymlinks by giving them the sametitle. 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 sharedtitlebridges them.
Forms — the form2 family
VERIFIED (2026-06-29).
form2,form_title,form_text_input,form_email_input,form_textarea_input,form_button2appear in the sample projectsemail_test/RetailStore/radio_example. Theform_dropdown_input/form_toggle_input/form_slider_inputvariants (incl. the slider'smin/max/start/step) were confirmed against an editor-created form; they share theform_text_inputshape, as do theurl/password/phone/date/datetimevariants. Smart forms (login_form3,credit_card_form1) are verified fromRetailStore/Magazine.form_buttons_rowis 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 carriesgroup_key: "inputs"(NOT"body") — that is the form2's input slot. A+Inputarg_placeholder(alsogroup_key: "inputs") marks where new rows go. - The form's identity is its
identifier; activity code reads/writes its values withget_form_values(<identifier>)/set_form_values(...), and forms with the same key stay in sync. NOTE: there is no storedform_keyfield/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 Framework7list-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, andform_button2carry nodo_on_actionfield. A form built in the current editor adds ado_on_actioncode hook to these — so treatdo_on_actionas 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 holdform_*_inputchildren. The entire form (all the fields plus its Submit/Cancel buttons) is baked into its bigwrapper_template, and the submit logic lives indo_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— itsdo_on_actionhandlessubmit/register/sel_sign_up/sel_log_in/sel_forgot_passwordactions vialogin_session(...)/register_user(...)/get_current_session(). (Some non-sample instances also carry anemail_usernamesdropdown param, but neither official sample'slogin_form3has it — treat it as optional, not standard.)credit_card_form1'sdo_on_actioncallscharge_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
iconto "none" (icon: ""). The form icons do not render properly, so leave the icon empty on every input that has aniconparam. The editor's default is already""; never set it to anicon-form-*class. - Do not use
form_buttons_row— it does not behave correctly. Putform_button2s directly inside theform2.
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_sliderfromMagazine/layers,data_swiper_sliderfromRetailStore.static_image2does not appear in any official sample (the samples use legacystatic_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_popoverfromstyletests,popover2frompopover_and_hidescroll_example(both confirmed verbatim, no missed params).floating_action_buttonandspeed_dialdo 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");popover2sits in the body. Note these are older elements — several carry the lighter param set (extra_javascript/extra_cssonly, noextra_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_linkfrompicture_database,custom_data_listfromcustom_data_list_example,accordion_listfromIcons. 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 likear). 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_list2uses structured per-item slots — its children carry distinctgroup_keys that map to parts of a list item:item_title(group_key: "item_title") — the item's title line; itstemplatereferences 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_listuses a free-form item layout — its children are ordinary body elements (acontentblock, a styledlink, etc.) that you arrange yourself, instead of the fixeditem_title/item_content/swipeoutsslots. Use it when you want full control over each row's markup.
Swipe actions — swipeouts and swipeout_link
A list item's swipe drawer:
swipeouts— the container (group_key: "swipeouts"inside adata_list2); rendersswipeout-actions-rightand holdsswipeout_links.swipeout_link— one swipe action. Distinctive params:swipe_action(e.g."delete") andcolor(e.g."bg-red"), plus ado_on_actionhook,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, andlibdo not appear in any official sample, so their shapes (incl.subView'scovered_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:
codeis the activity-logic field forclient_activity/client_call/signal(andrepeatusesdo_on_cycle). This is where the location-transparent Exotool language lives. Adecisioncarries no inline code in the official sample — it branches on its outgoing connectors' conditions.subViewis special — it does NOT nest its members underelements; it references them by key in acovered_elementsmap (a visual region that "covers" the activities/pages it groups). Invoke it withrun_subprocess(...). Its geometry is SEMANTIC, not cosmetic: a subView covers (scopes) any element whose bounding box it fully encloses graphically — socovered_elementsis 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/listenpair on achannel_key—signalsends (send_signal),listenreceives, 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, anerr, 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: theapielements indiagrams/app.diagramand the generatedbackend/backend.php(API_create_beneficiary,API_create_new_broker). Implemented as a subclass ofclient_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 acontentblockand alinkmenu("Link Menu") holding four childlinks. - The "page with navbar" page shows a top
navbar(aback_linklabeled "Back") and a bottombottom_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 leftpanel, a rightpanel(each with Nav/Tool bars, Content, +Page Body slots), and apopup(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 (+ its connector)
"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.
Current / recommended types
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.